# Canvas Medical Developer Documentation (Full Reference) > Canvas Medical's developer documentation: the server-side Python SDK, the FHIR API, and implementation guides for building EMR customizations. Patient and staff keys are UUIDs without dashes; other identifiers use standard dashed UUIDs. # Appointment Metadata Create form Source: https://docs.canvasmedical.com/sdk/appointment-metadata-create-form-effect/ This allows developers to dynamically display additional fields when scheduling an appointment. For more guidance, see the ["How to add additional fields to the scheduling appointment fields" guide](/guides/appointments-additional_fields/). Build an `AppointmentsMetadataCreateFormEffect` with the form fields to show, then return its `apply()` from an `APPOINTMENT__FORM__GET_ADDITIONAL_FIELDS` handler. ## Methods ### apply() → Effect Displays the form fields on the appointment form. - `form_fields` is required. - `options` may only be set on fields whose `type` is `InputType.SELECT`. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `form_fields`| `list[``FormField``]`| List of fields.| Yes ## FormField Attribute| Type| Description ---|---|--- `key`| `str`| Unique identifier of the field; the appointment metadata key. `label`| `str`| The label displayed on the field. `type`| `InputType`| The type of the input: `TEXT`, `SELECT`, or `DATE`. Defaults to `TEXT`. `required`| `bool`| Whether the input is required. Defaults to `False`. `editable`| `bool`| Whether the input can be edited. Defaults to `True`. `options`| `list[str]`| Possible options when the input type is `SELECT`. Only allowed on `SELECT` fields. `value`| `str`| Default value for the field. ## Example ```python from canvas_sdk.effects.appointments_metadata import ( FormField, InputType, AppointmentsMetadataCreateFormEffect, ) AppointmentsMetadataCreateFormEffect(form_fields=[ FormField( key='status', label='Status', type=InputType.SELECT, required=False, editable=True, options=["open", "close"], value="" ), ]).apply() ``` --- # Caching API Source: https://docs.canvasmedical.com/sdk/caching/ The Canvas SDK provides a caching API for plugin developers to store and retrieve temporary data efficiently. For persistent storage of plugin data, use instead the [Custom Data](/sdk/custom-data/) features. * * * ## Getting the Cache Client To use the cache in your plugin, simply import and call: ```python from canvas_sdk.caching.plugins import get_cache cache = get_cache() ``` * * * ## TTL and Expiration - By default, all cached keys expire after **14 days**. - You can set a shorter TTL when writing to the cache via the `timeout_seconds` parameter. - If a longer TTL is provided, a `CachingException` will be raised. ### Caching an external access token When a plugin calls an external API that issues OAuth access tokens, cache the token rather than requesting a new one on every call. Set `timeout_seconds` to the token's lifetime so the plugin fetches a fresh one only after the old one expires. Keep the client credentials in [secrets](/sdk/secrets/). ```python from canvas_sdk.caching.plugins import get_cache from canvas_sdk.utils import Http def get_access_token(secrets: dict) -> str: cache = get_cache() token = cache.get("partner_access_token") if token is None: response = Http().post( "https://auth.example.com/oauth/token", data={ "grant_type": "client_credentials", "client_id": secrets["PARTNER_CLIENT_ID"], "client_secret": secrets["PARTNER_CLIENT_SECRET"], }, ) body = response.json() token = body["access_token"] # Expire a minute early so a token never runs out mid-request. cache.set("partner_access_token", token, timeout_seconds=body["expires_in"] - 60) return token ``` Call it from a handler as `get_access_token(self.secrets)`. * * * ## Supported Methods ### `get(key: str, default: Any | None = None) -> Any` Retrieve a value from the cache: ```python user = cache.get("key") ``` You can specify a fallback if the key doesn't exist: ```python user = cache.get("key", default="default_value") ``` * * * ### `set(key: str, value: Any, timeout_seconds: int | None = None) -> None` Store a value in the cache: ```python cache.set("key", {"name": "Alice"}, timeout_seconds=600) ``` * * * ### `get_or_set(key: str, default: Any | Callable, timeout_seconds: int | None = None) -> Any` Fetch a value or set it if not present: ```python value = cache.get_or_set("key", default=lambda: compute_value(), timeout_seconds=300) ``` * * * ### `set_many(data: dict[str, Any], timeout_seconds: int | None = None) -> list[str]` Set multiple values at once: ```python cache.set_many({ "key1": {"name": "Alice"}, "key2": {"name": "Bob"} }, timeout_seconds=900) ``` * * * ### `get_many(keys: Iterable[str]) -> dict[str, Any]` Fetch multiple values in one operation: ```python users = cache.get_many(["key1", "key2"]) ``` * * * ### `delete(key: str) -> None` Remove a key from the cache: ```python cache.delete("key") ``` * * * ### `__contains__(key: str) -> bool` Check if a key exists in cache: ```python if "key" in cache: ... ``` --- # Calendar Create Source: https://docs.canvasmedical.com/sdk/calendar-create-effect/ This allows developers to create calendars for providers in Canvas. Calendars can be either Clinic or Administrative type and can optionally be associated with a location. Build a `Calendar` with the attributes you want to set, then return its `create()` from your handler. ## Methods ### create() → Effect Creates the calendar. - `provider` and `type` are required. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `id`| `str \| UUID \| None`| Optional unique identifier for the calendar.| No `provider`| `str \| UUID`| The id of the [provider](/sdk/data-staff/).| Yes `type`| `CalendarType`| The type of calendar: `CalendarType.Clinic` or `CalendarType.Administrative`.| Yes `location`| `str \| UUID \| None`| The id of the [location](/sdk/data-practicelocation/) to associate with the calendar.| No `description`| `str \| None`| Description of the calendar's purpose.| No ## CalendarType An enumeration of calendar types: Value| Description ---|--- `Clinic`| Calendar for clinical appointments `Administrative`| Calendar for administrative tasks ## Example ```python from canvas_sdk.effects.calendar import Calendar, CalendarType Calendar( provider="provider-uuid", type=CalendarType.Clinic, location="location-uuid", description="Primary clinic calendar" ).create() ``` --- # Calendar Event Management Source: https://docs.canvasmedical.com/sdk/calendar-event-management-effects/ This allows developers to create, update, and delete calendar events for providers in Canvas. Events can be one-time or recurring, with support for daily and weekly recurrence patterns. Build an `Event` with the attributes you want to set, then return one of its methods from your handler: `create()`, `update()`, or `delete()`. ## Methods ### create() → Effect Creates the event on a calendar. - `calendar_id`, `title`, `starts_at`, and `ends_at` are required. ### update() → Effect Updates an existing event. - `event_id`, `title`, `starts_at`, and `ends_at` are required. ### delete() → Effect Deletes an existing event. - `event_id` is required. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `calendar_id`| `str \| UUID \| None`| The id of the calendar where the event will be created.| For `create()` `event_id`| `str \| UUID \| None`| The id of the event to update or delete.| For `update()` and `delete()` `title`| `str \| None`| The title of the event.| For `create()` and `update()` `starts_at`| `datetime \| None`| The start date and time of the event.| For `create()` and `update()` `ends_at`| `datetime \| None`| The end date and time of the event.| For `create()` and `update()` `recurrence_frequency`| `EventRecurrence` `\| None`| The frequency of recurrence: `EventRecurrence.Daily` or `EventRecurrence.Weekly`.| No `recurrence_interval`| `int \| None`| The interval between recurrences (for example, 1 for every week, 2 for every other week).| No `recurrence_days`| `list[``DaysOfWeek``] \| None`| List of days when the event should recur (used with weekly recurrence).| No `recurrence_ends_at`| `datetime \| None`| The date and time when the recurrence pattern ends.| No `allowed_note_types`| `list[str] \| None`| List of note types that are allowed for this event.| No ## EventRecurrence An enumeration of recurrence frequency options: Value| Description ---|--- `Daily`| Event recurs daily `Weekly`| Event recurs weekly ## DaysOfWeek An enumeration of days of the week for recurring events: Value| Description ---|--- `MO`| Monday `TU`| Tuesday `WE`| Wednesday `TH`| Thursday `FR`| Friday `SA`| Saturday `SU`| Sunday ## Example ```python from canvas_sdk.effects.calendar import Event, EventRecurrence, DaysOfWeek from datetime import datetime # Create a one-time event Event( calendar_id="calendar-uuid", title="Patient Consultation", starts_at=datetime(2025, 1, 15, 9, 0), ends_at=datetime(2025, 1, 15, 10, 0) ).create() # Create a recurring event Event( calendar_id="calendar-uuid", title="Weekly Team Meeting", starts_at=datetime(2025, 1, 15, 14, 0), ends_at=datetime(2025, 1, 15, 15, 0), recurrence_frequency=EventRecurrence.Weekly, recurrence_interval=1, recurrence_days=[DaysOfWeek.Monday, DaysOfWeek.Wednesday], recurrence_ends_at=datetime(2025, 12, 31, 23, 59), allowed_note_types=["100", "101"] ).create() # Update an existing event Event( event_id="event-uuid", title="Updated Meeting Title", starts_at=datetime(2025, 1, 15, 15, 0), ends_at=datetime(2025, 1, 15, 16, 0) ).update() # Delete an event Event(event_id="event-uuid").delete() ``` --- # Canvas CLI Source: https://docs.canvasmedical.com/sdk/canvas_cli/ > **Warning:** The Canvas CLI signs in with `canvas login` and ships plugins with `canvas deploy`. Instance API credentials in `credentials.ini` and `canvas install` are deprecated, and support for them ends on **December 14, 2026**. See [Moving to Canvas Platform](/guides/moving-to-canvas-platform/). ## Getting Started ### Installation using `pip` To install the Canvas CLI using `pip`, execute `pip install canvas`. Python 3.11, 3.12, or 3.13 is required. To upgrade the Canvas CLI if you installed using `pip`, execute `pip install --upgrade canvas`. ### Installation using `uv` To install the Canvas CLI using `uv`, execute `uv tool install canvas`. `uv` will find or procure an acceptable Python version. To upgrade the Canvas CLI if you installed using `uv`, execute `uv tool upgrade canvas`. ### Signing in to Canvas Platform The Canvas CLI signs in to [Canvas Platform](https://platform.canvasmedical.com), which hosts each plugin's git repository and deploys it to the instances your organizations manage. Sign in once per machine: ```console $ canvas login Signed in to https://platform.canvasmedical.com as you@example.com. Acme Health (acme): plugin prefix acme__ ``` `canvas login` opens your browser and also prints the sign-in URL, for a machine without a browser. It lists each organization you belong to with its plugin prefix. The session is stored in `~/.canvas/platform-credentials.json`, readable only by you, and refreshes itself. `canvas logout` revokes it. A CI/CD pipeline signs in with a service account instead. A plugin deployed through Canvas Platform has a publisher-prefixed name, `__` (for example `acme__intake`), used for the manifest `name`, the package folder and its imports. The prefix decides which organization publishes the plugin. `canvas login --platform ` or the `CANVAS_PLATFORM_URL` environment variable points the CLI at a platform other than https://platform.canvasmedical.com, and later commands keep using the platform you last signed in to. `canvas whoami` shows which platform the CLI is using, why, and who you are signed in as there: ```console $ canvas whoami Platform: https://platform.canvasmedical.com (from your last `canvas login`) Signed in as you@example.com (browser session from `canvas login`) Acme Health (acme): plugin prefix acme__, can push, deploy, configure, uninstall ``` **You are now ready to use the Canvas CLI.** Continue with `canvas init` and `canvas deploy`. ### Service accounts for CI/CD The Canvas CLI supports service accounts for automated pipelines such as GitHub Actions, GitLab CI or any other CI/CD system. A service account is an identity that belongs to your organization rather than to a person, so a pipeline that publishes and deploys plugins keeps working when the person who set it up leaves, and Canvas Platform's audit log names the service account behind each push and deploy. A pipeline signs in with the account's token instead of `canvas login`, which needs a browser. **Create a service account.** An Organization admin, Plugin developer or Deploy manager creates one on the Credentials page in Canvas Platform. A service account can hold the Plugin developer and Deploy manager roles. An Organization admin can give it either role; a Plugin developer or Deploy manager can give only the roles they hold, so a Plugin developer cannot create an account that deploys. Its token is shown once and is valid for a year. **Use it in a pipeline.** Store the token as a secret in your CI/CD system and expose it to the job as the `CANVAS_PLATFORM_TOKEN` environment variable. No `canvas login` step is needed: ```yaml - uses: actions/checkout@v4 with: fetch-depth: 0 - run: pip install canvas - run: canvas deploy my-plugin/acme__my_plugin --instance acme-staging --yes env: CANVAS_PLATFORM_TOKEN: ${{ secrets.CANVAS_PLATFORM_TOKEN }} ``` - Check out the full history (`fetch-depth: 0` in GitHub Actions). `canvas deploy` pushes the checkout to the plugin's repository on Canvas Platform, and git refuses to push a shallow clone to a repository that lacks its history. - The push to Canvas Platform's `main` is not forced, so it is refused when Canvas Platform's `main` has a commit the checkout lacks, such as one a developer deployed from their machine without pushing it to the repository the pipeline checks out. Once a pipeline deploys a plugin, deploy it only from the pipeline. - When `CANVAS_PLATFORM_TOKEN` is set, every command uses it in place of a `canvas login` session, including the git pushes `canvas deploy` makes. It takes precedence over a session already stored on the machine, so a runner always acts as its service account. - `--yes` commits uncommitted changes without prompting. A deployment that needs consent for another plugin's custom data exits non-zero and lists the requests, because only a person at a terminal answers those. - `CANVAS_PLATFORM_URL` points the job at a platform other than https://platform.canvasmedical.com. **Rotate or revoke it.** The token does not refresh. Rotating it on the Credentials page ends the old token, and revoking the service account ends it for good; either way the CLI's next request says that Canvas Platform did not accept `CANVAS_PLATFORM_TOKEN`. `canvas logout` signs out only a browser session and leaves the token alone. ### Instance API credentials (`credentials.ini`): deprecated > **Warning:** **Deprecated:** authenticating the Canvas CLI with OAuth client credentials in `~/.canvas/credentials.ini`, and installing plugins with `canvas install`, are deprecated. Support ends on **December 14, 2026**. Sign in with `canvas login` and deploy with `canvas deploy` instead. [Moving to Canvas Platform](/guides/moving-to-canvas-platform/) walks through the switch. With `credentials.ini`, the CLI authenticates to each instance with an OAuth client registered on that instance. `canvas install`, `enable`, `disable`, `list`, `config list`, `logs` and the `namespace` commands authenticate this way today, as do `config set`, `config unset` and `uninstall` for a plugin Canvas Platform does not manage. Keep `credentials.ini` until the commands you use sign in through Canvas Platform. A machine can hold both a Canvas Platform sign-in and `credentials.ini` at once; the CLI picks per command and per plugin, as described in Which plugins go through Canvas Platform. The file has a section for each Canvas instance subdomain, with that instance's `client_id` and `client_secret`. For example, if your Canvas instance URL is `https://buttered-popcorn.canvasmedical.com/`, its section is `[buttered-popcorn]`. [Customer Authentication](/api/customer-authentication/#registering-a-third-party-application-on-canvas) explains how to register a client. **Example:** ```ini [buttered-popcorn] client_id=butter client_secret=salt [dev-buttered-popcorn] client_id=devbutter client_secret=devsalt is_default=true [localhost] client_id=localclientid client_secret=localclientsecret ``` You can define your default host with `is_default=true`. If no default is explicitly defined, the Canvas CLI will use the first instance in the file as the default for each of the CLI commands. ### Deprecation notices `canvas install` prints a deprecation warning on every run. Separately, at most once a day, any command, `canvas install` included, prints a notice that authentication and deployment are changing, with a link to the migration steps. Both go to standard error, so they do not interfere with piped or redirected output. ### Which plugins go through Canvas Platform `canvas config set`, `canvas config unset` and `canvas uninstall` work for plugins Canvas Platform manages and for plugins installed straight onto an instance, and each decides per plugin: - A name without a prefix, or a machine that is not signed in, goes straight to the instance. - Otherwise the CLI asks Canvas Platform for the plugin. If Canvas Platform has it, the command goes through Canvas Platform; if not, it goes to the instance. `--instance` names the target instances for either kind of plugin, and you can repeat it. For a plugin Canvas Platform does not manage, the CLI connects to each `--instance` directly with its `credentials.ini` client. `--host` names a single instance instead, and without either option the command uses the default host from `credentials.ini`; pass `--instance` or `--host`, not both. For a plugin Canvas Platform manages, the CLI refuses `--host`. An instance accepts direct writes for every plugin, including one Canvas Platform deployed. A `canvas install`, `config set`, `config unset` or `uninstall` that goes straight to the instance changes that instance's copy of the plugin. The next deploy from Canvas Platform installs its own revision over it and replaces the plugin's variables with the values Canvas Platform stores. Canvas Platform can also deploy over a plugin first installed with `canvas install`. git signs in to Canvas Platform's git server through `canvas git-credential`, a hidden command that `canvas deploy`, `canvas init` and `canvas clone` register as the repository's credential helper. It mints a one-hour git token for the plugin named in the repository path. ## Update Notifications The Canvas CLI automatically checks [PyPI](https://pypi.org/project/canvas/) for newer versions. If an update is available, a notice is printed to standard error after the command output: ```shell [notice] A newer version of canvas is available (0.112.0 → 0.113.0). Upgrade with: pip install --upgrade canvas ``` - The check runs at most once every 12 hours; the result is cached locally to avoid unnecessary network requests. - Because the notice is printed to standard error, it will not interfere with piped or redirected command output. - To disable update checks, set the environment variable `CANVAS_NO_UPDATE_CHECK=1`. ## Checking the SDK/runtime version on an instance `canvas --version` reports the version of the locally installed CLI. To find the SDK/runtime version actually running on a given Canvas instance — useful when verifying that an expected SDK change is live — import `version` from `canvas_sdk.handlers.base` inside a plugin running on that instance: ```python from canvas_sdk.handlers.base import version from logger import log log.info(f"SDK version: {version}") ``` ## Usage ```console $ canvas [OPTIONS] COMMAND [ARGS]... ``` **Options** : - `--version` - `--help`: Show this message and exit. ## Commands - `login`: Sign in to Canvas Platform through your browser - `logout`: Sign out of Canvas Platform - `whoami`: Show which Canvas Platform you are signed in to, and as whom - `init`: Create a new plugin - `deploy`: Publish a plugin to Canvas Platform and deploy it - `clone`: Clone a plugin's repository from Canvas Platform - `uninstall`: Uninstall a plugin - `enable`: Enable a plugin from a Canvas instance - `disable`: Disable a plugin from a Canvas instance - `list`: List all plugins from a Canvas instance - `validate`: Validate a plugin's manifest and that all handlers load in the sandbox - `validate-manifest`: Validate the Canvas Manifest json file - `logs`: Listen and print log streams from a Canvas instance - `config list`: List plugin variables on a Canvas instance - `config set`: Set plugin variables - `config unset`: Clear plugin variables - `install`: Install a plugin into a Canvas instance (deprecated; use `deploy`) ### `canvas login` Sign in to Canvas Platform through your browser (OAuth authorization code with PKCE). The sign-in URL is also printed, for a machine without a browser. See Signing in to Canvas Platform. **Usage** : ```console $ canvas login [OPTIONS] ``` **Options** : - `--platform TEXT`: Canvas Platform URL. Defaults to `$CANVAS_PLATFORM_URL`, the platform you last signed in to, or https://platform.canvasmedical.com - `--help`: Show this message and exit. ### `canvas logout` Revoke this machine's Canvas Platform session and delete its stored tokens. A service account token in `CANVAS_PLATFORM_TOKEN` is left alone, and the command says that commands still act as that account. **Usage** : ```console $ canvas logout [OPTIONS] ``` **Options** : - `--platform TEXT`: Canvas Platform URL - `--help`: Show this message and exit. ### `canvas whoami` Show the Canvas Platform the CLI uses and where that choice came from (`--platform`, `CANVAS_PLATFORM_URL`, your last `canvas login`, or the default), then the account it signs in as, whether the credential is a browser session or a service account token, and each organization's plugin prefix and what you can do there. When `CANVAS_PLATFORM_TOKEN` is set alongside a stored session, it says the token takes precedence. `whoami` asks Canvas Platform, so a revoked or expired credential is reported rather than shown as signed in. It never prints a token, so it is safe to run in a CI job's log. It exits non-zero when you are not signed in, with the `canvas login` command to run. **Usage** : ```console $ canvas whoami [OPTIONS] ``` **Options** : - `--platform TEXT`: Canvas Platform URL - `--help`: Show this message and exit. ### `canvas init` Create a new plugin. Signed in to Canvas Platform, the package is named `__`, registered with Canvas Platform, and given a git repository whose `origin` is Canvas Platform, ready for `canvas deploy`. Signed out, it is named from the project name alone and `init` says so: `canvas deploy` refuses such a plugin, so run `canvas login` and `canvas init` again for one you can deploy. Run `canvas whoami` first if you are unsure which platform you are signed in to. **Usage** : ```console $ canvas init [OPTIONS] [PLUGIN_TYPE] ``` **Arguments** : - `PLUGIN_TYPE`: `handler` (default) or `application` **Options** : - `--org TEXT`: Organization that publishes the plugin, when you can publish in several. Without it, `init` asks. - `--help`: Show this message and exit. ### `canvas deploy` Publish a plugin's code to Canvas Platform and deploy it to one or more instances. **Usage** : ```console $ canvas deploy [OPTIONS] PLUGIN_DIR ``` **Arguments** : - `PLUGIN_DIR`: Path to the plugin package, the folder that holds `CANVAS_MANIFEST.json` [required] **Options** : - `--instance TEXT`: Instance slug to deploy to, repeatable. Without it, `deploy` targets the only instance you can deploy to, and otherwise lists the choices. - `--ref TEXT`: Deploy a pushed branch, tag or commit as-is, without pushing. - `--no-push`: Deploy the pushed `main` as-is, without pushing. - `--push-only`: Push HEAD to `main` without deploying it to any instance, so the plugin's code and its [catalog listing](/sdk/canvas_manifest/#catalog) are updated on Canvas Platform. Takes no `--instance`, `--ref` or `--no-push`. - `-y, --yes`: Commit uncommitted changes without prompting, with the default message. Does not approve consent requests. - `--help`: Show this message and exit. **Example** : ```console $ canvas deploy paperwork-eviscerator/acme__paperwork_eviscerator --instance acme-staging ``` **Notes** : **What`deploy` does** 1. Checks the manifest `name` is publisher-prefixed and the package folder has the same name, before anything is committed or pushed. Then registers the plugin with Canvas Platform under the organization its prefix names. 2. Points the repository's `origin` at Canvas Platform, with `canvas` as git's credential helper for it. 3. Commits uncommitted changes after you confirm. Without a terminal to confirm at, it refuses uncommitted changes unless you pass `--yes`. 4. Pushes HEAD to `main`, deploys that commit to each instance, and waits for each instance's outcome. The command exits non-zero unless every instance succeeds, so it can gate a CI job. **Waiting for the outcome** For a plugin Canvas Platform manages, `canvas deploy`, `canvas config set`, `canvas config unset` and `canvas uninstall` each create a deployment, wait for it to finish, and print each instance's outcome. They stop waiting after 300 seconds, print a message such as `Deploy of acme__intake is still running after 300s (deployment ).`, and exit non-zero. The CLI does not cancel the deployment when it stops waiting. Canvas Platform sends a plugin only the stored values whose keys the deployed revision's manifest declares in [`variables`](/sdk/canvas_manifest/#variables). When it leaves stored values out, the CLI lists them under that instance's outcome: ```text acme__intake on acme-staging: succeeded Stored values left out because this revision does not declare them: OLD_FLAG ``` If the plugin still needs one of these values, declare its key in the manifest and deploy again. **Plugin repository layout** Canvas Platform hosts one git repository per plugin, rooted at the directory that contains the package: ```text paperwork-eviscerator/ # repository root ├── acme__paperwork_eviscerator/ │ ├── CANVAS_MANIFEST.json │ ├── OVERVIEW.md # the plugin's overview in the catalog │ └── handlers/ ├── pyproject.toml └── tests/ ``` `canvas init` creates this layout. If the package is not in a git repository yet, `deploy` offers to create one at its parent directory. A package nested deeper inside a larger repository is refused; move it into its own repository, or `canvas clone` it. Because `deploy` pushes a git commit, `.gitignore` decides what reaches Canvas Platform. Keep virtual environments, `node_modules` and local secrets out of the repository with it. **Consent for cross-plugin data access** A plugin that reads another plugin's [custom data](/sdk/custom-data-sharing-data/) needs consent before it deploys. At a terminal, `deploy` shows each consent request and asks you to answer it. Under `--yes`, or without a terminal, it lists the requests with the deployment id and exits non-zero without answering them. **Validate first** Run `canvas validate` before you deploy. It runs the manifest checks, the static lint, and sandbox-load validation locally, where errors are faster to fix than after a push. ### `canvas clone` Clone a plugin's repository from Canvas Platform with `origin` and the credential helper set, ready for `canvas deploy`. **Usage** : ```console $ canvas clone [OPTIONS] NAME [DIRECTORY] ``` **Arguments** : - `NAME`: Plugin name, e.g. `acme__intake` [required] - `DIRECTORY`: Where to clone it, a directory named after the plugin by default **Options** : - `--help`: Show this message and exit. ### `canvas install` > **Warning:** **Deprecated:** `canvas install` uploads a plugin straight onto an instance using `credentials.ini`. Support ends on **December 14, 2026**. Use `canvas deploy`, and see [Moving to Canvas Platform](/guides/moving-to-canvas-platform/). Install a plugin into a Canvas instance. **Usage** : ```console $ canvas install [OPTIONS] PLUGIN_NAME ``` **Arguments** : - `PLUGIN_NAME`: Path to plugin to install [required] **Options** : - `--variable TEXT`: Non-sensitive variables to set, e.g. Key=value - `--secret TEXT`: Sensitive variables to set (treated as sensitive=true), e.g. Key=value - `--enable / --disable`: Install the plugin in an enabled or disabled state. Defaults to `--enable`. - `--host TEXT`: Canvas instance to connect to - `--help`: Show this message and exit. **Notes** : **Where to run it** - Run it from one directory **above** the plugin folder, the one that holds `CANVAS_MANIFEST.json`, and pass the folder name. If the manifest is at `extensions/encounter_list/CANVAS_MANIFEST.json`, run this from `extensions`: ```console $ canvas install encounter_list --host your-host ``` - Or run `canvas install .` from inside the plugin folder. - The command is `canvas install`. There is no `canvas plugin install`. **Plugin folder layout** - Keep the manifest, the package's `__init__.py`, and the folders it references (such as `routes/` and `protocols/`) in the same directory. Class paths in the manifest, such as `my_plugin.routes.charting`, resolve from there. - An extra level of nesting, with the manifest outside the inner package folder, makes those paths resolve one level too deep. The plugin loads, but its SimpleAPI endpoints return an empty 404, often with nothing in `canvas logs`. If that happens, check the folder structure and that every import in the referenced modules resolves. **Validation before upload** `canvas install` runs the same checks as `canvas validate` before it uploads anything: - Manifest validation (schema, tags, handler resolution) - Static lint (scans your source for sandbox-forbidden constructs and Custom Data mistakes) - Sandbox-load validation (imports every handler in the sandbox) If the static lint reports an error, or a handler fails to load (for example, because of a disallowed import like `subprocess`), the install stops before the plugin is built or uploaded. Run `canvas validate` first for detailed per-handler results. **What gets packaged** The CLI packages every file in the plugin folder except: - `__pycache__` directories - `*.pyc` and `*.pyo` files - `node_modules` directories - Hidden files and directories (for example, `.git` and `.env`) - Symlinks To exclude more files, add a `.canvasignore` file: - Put it in the directory you run `canvas install` from. The CLI reads it from the current working directory, not from the plugin folder. - It uses [.gitignore](https://git-scm.com/docs/gitignore) syntax, and its patterns match paths relative to the plugin folder. ```md # Exclude test files test_*.py ``` The CLI prints a warning, but still installs, when a single file is over 1 MB, the package holds more than 100 files, or its total size is over 1 MB. **Troubleshooting** - **The install fails with an HTTP 500:** the package usually has too many files, most often because a Python virtual environment was created inside the plugin folder. Rename the folder so it starts with a dot (for example, `.canvas-env`), since hidden folders aren't packaged, or list it in `.canvasignore`. ### `canvas uninstall` Uninstall a plugin. A plugin Canvas Platform manages is removed by an uninstall deployment from each `--instance`, which is required. Any other plugin is removed from the instance directly, using `credentials.ini` (deprecated; see Instance API credentials). > **Warning:** Uninstalling a plugin deletes the [banner alerts](/sdk/effect-banner-alerts/) it added to patient charts and [claims](/sdk/effect-claims/#add-banner). Disabling a plugin hides them until it's enabled again. **Usage** : ```console $ canvas uninstall [OPTIONS] NAME ``` **Arguments** : - `NAME`: Plugin name to uninstall [required] **Options** : - `--instance TEXT`: Instance slug to uninstall from, repeatable - `--host TEXT`: Canvas instance to connect to, for a plugin Canvas Platform does not manage - `--force`: Uninstall an enabled plugin from an instance directly - `--help`: Show this message and exit. ### `canvas enable` Enable a plugin from a Canvas instance.. **Usage** : ```console $ canvas enable [OPTIONS] NAME ``` **Arguments** : - `NAME`: Plugin name to enable [required] **Options** : - `--host TEXT`: Canvas instance to connect to - `--help`: Show this message and exit. ### `canvas disable` Disable a plugin from a Canvas instance.. **Usage** : ```console $ canvas disable [OPTIONS] NAME ``` **Arguments** : - `NAME`: Plugin name to disable [required] **Options** : - `--host TEXT`: Canvas instance to connect to - `--help`: Show this message and exit. ### `canvas list` List all plugins on a Canvas instance. **Usage** : ```console $ canvas list [OPTIONS] ``` **Options** : - `--host TEXT`: Canvas instance to connect to - `--help`: Show this message and exit. ### `canvas validate` Validate a plugin's manifest and that all handlers load in the sandbox. **Usage** : ```console $ canvas validate [OPTIONS] PLUGIN_NAME ``` **Arguments** : - `PLUGIN_NAME`: Path to plugin to validate [required] **Options** : - `--help`: Show this message and exit. This command runs full pre-flight validation combining: 1. **Manifest validation** — Schema checks, tag validation, handler resolution, and unreferenced handler warnings (everything `validate-manifest` does). 2. **Static lint** — Scans the plugin's source for sandbox-forbidden constructs and Custom Data mistakes before any code runs. 3. **Sandbox-load validation** — Imports every handler the way the plugin runner will, catching violations that would otherwise surface only at runtime on the instance. #### Static lint Before it loads any handlers, `canvas validate` scans every `.py` file in the plugin — skipping directories like `tests`, `build`, and `dist` _within_ the plugin — for patterns that compile cleanly but fail, or silently misbehave, once your code runs on the instance. Each finding is reported with a rule code in brackets. Warnings are printed but do not block validation; any error fails the command and exits with code 1. ```console $ canvas validate my_plugin ⚠ my_plugin/handlers/protocol.py:42 [custom-model-id-vs-dbid] Widget.objects.filter(id=…) — CustomModels use `dbid` as their primary key (only core SDK models have `id`). Use `dbid=…` instead. These issues will fail on the instance (sandbox / Custom Data): ✗ my_plugin/handlers/protocol.py:18 [type-blocked] `type()` takes one argument in the sandbox. Declare classes with `class … :` rather than `type(name, bases, dict)`. ``` **Sandbox constructs (errors).** These compile under RestrictedPython but are rejected when a handler runs on the instance, so a plain sandbox load can miss them. See [Sandboxing and Allowed Imports](/sdk/sandboxing-and-allowed-imports/#forbidden-constructs) for the full list and the allowed alternatives. Rule code| Flags ---|--- `type-blocked`| A `type()` call with anything other than a single argument, such as `type(name, bases, dict)`. The sandbox's `type` takes one argument, so declare classes with `class …:` instead. `type(x)` is not flagged. `augmented-subscript`| Augmented assignment on a subscript, e.g. `d[k] += v` — rewrite as `d[k] = d[k] + v` `augmented-attribute`| Augmented assignment on an attribute, e.g. `obj.attr += v` — rewrite as `obj.attr = obj.attr + v` `@dataclass(frozen=True)` and `@dataclass(slots=True)` load and run fine in the sandbox and are intentionally not flagged. **Custom Data (errors).** Both leave tables silently uncreated, so queries fail at runtime. See [Custom Models](/sdk/custom-data-custom-models/) and the [Quick Start](/sdk/custom-data-quick-start/) for setup. Rule code| Flags ---|--- `custom-model-wrong-dir`| A `CustomModel` subclass defined outside `/models/` — Canvas only loads models from that directory `missing-custom-data-block`| CustomModels are present but the manifest has no `custom_data` block (an empty block counts as missing — it must be non-empty) **Custom Data (warnings).** These don't block validation but usually indicate a bug: Rule code| Flags ---|--- `custom-model-id-vs-dbid`| `.filter(id=…)` / `.get(id=…)` on a local CustomModel — CustomModels key on `dbid`, not `id`. Use `dbid=…` `lazy-fk-string-ref`| A `ForeignKey`/`OneToOneField`/`ManyToManyField` with a string reference to a CustomModel defined in this plugin — import the class and pass it directly #### Sandbox-load validation After the static lint passes, sandbox-load validation executes each handler module in the plugin sandbox to catch: - **Disallowed imports** — Modules like `subprocess`, `socket`, or `os` that are blocked by the sandbox. - **RestrictedPython compile-time errors** — Syntax or constructs that RestrictedPython cannot compile. - **Import errors** — Missing dependencies or broken imports. - **Refused code in helper modules** — Functions in the modules your handlers import that contain code the sandbox refuses. Every handler fails with a `PluginInstallationError` listing each construct by module and line. See [Helper modules](/sdk/sandboxing-and-allowed-imports/#helper-modules). For each handler, the output shows whether it loaded successfully: ```console $ canvas validate my_plugin Loading 2 handler(s) in the sandbox: ✓ my_plugin.handlers.events:MyHandler ✗ my_plugin.handlers.api:APIHandler ImportError: 'subprocess' is not an allowed import 1 of 2 handler(s) failed to load in the sandbox. ``` The command exits with code 1 if any handler fails validation. If your handlers read attributes that don't exist while they load, the output ends with a warning that lists them. In the sandbox those reads return `None` for now, and a future release will raise `AttributeError` instead (see [Attributes that don't exist](/sdk/sandboxing-and-allowed-imports/#attributes-that-dont-exist)). The warning doesn't fail validation: ```console $ canvas validate my_plugin Loading 1 handler(s) in the sandbox: ✓ my_plugin.handlers.events:MyHandler Warning: these names do not exist. Each reads as None today and will raise an AttributeError in a future release: - Settings.retry_limit ``` #### Limitations A passing `canvas validate` confirms that handlers import cleanly under the sandbox — it does not guarantee the plugin is fully sandbox-clean. RestrictedPython checks attribute and item access inside `compute()` at request time, not at import time, so violations during handler execution won't be caught by this command. > **Info:** Run `canvas validate` before `canvas deploy` so violations are caught on your machine before they reach an instance. ### `canvas validate-manifest` Validate the Canvas Manifest json file. **Usage** : ```console $ canvas validate-manifest [OPTIONS] PLUGIN_NAME ``` **Arguments** : - `PLUGIN_NAME`: Path to plugin to validate [required] **Options** : - `--help`: Show this message and exit. **Validations performed** : 1. **Schema validation** — Checks that `CANVAS_MANIFEST.json` contains all required fields and valid values. 2. **Handler resolution** — Verifies that every handler class declared in the manifest (`protocols`, `applications`, and `handlers`) resolves to a file the plugin runner can find at runtime. #### Handler resolution and directory layout The plugin runner loads handlers by mapping dotted module paths to files relative to the plugin's install directory. For a plugin named `my_plugin` with a handler class `my_plugin.handlers.events:MyHandler`, the runner expects `handlers/events.py` inside the plugin directory — the directory containing `CANVAS_MANIFEST.json`. A common mistake is placing `CANVAS_MANIFEST.json` in a parent directory above the plugin package. This passes schema validation and works locally, but fails at runtime with `ModuleNotFoundError` — the handler files are nested one level too deep. **Correct layout:** ```text my_plugin/ ├── CANVAS_MANIFEST.json # ← manifest inside the package ├── handlers/ │ └── events.py └── ... ``` **Incorrect layout:** ```text project/ ├── CANVAS_MANIFEST.json # ← manifest above the package (wrong!) └── my_plugin/ └── handlers/ └── events.py ``` If `validate-manifest` detects handlers that won't resolve, it reports which classes are affected and the file paths the runner expects: ```console Error: these handler classes won't be found by the plugin runner with the current directory layout: - my_plugin.handlers.events:MyHandler runner expects: my_plugin/handlers/events.py CANVAS_MANIFEST.json must live inside the plugin's package directory (the directory whose name matches the manifest "name"), alongside the handler packages — not in a parent directory above them. ``` > **Info:** Use `canvas validate` before `canvas deploy` for a full pre-flight check: manifest validation, the static lint, and sandbox-load validation, with detailed per-handler output. ### `canvas logs` Subscribes to a log stream and prints to your console. Optionally fetches historical logs first. **Usage** : ```console $ canvas logs [OPTIONS] ``` **Options** : - `--host TEXT`: Canvas instance to connect to - `--help`: Show this message and exit. - `--since TEXT`: Lookback window (e.g. '24h', '2h30m'). Mutually exclusive with –start/–end. - `--start TEXT`: Start time (ISO/RFC3339) or 'now'. - `--end TEXT`: End time (ISO/RFC3339) or 'now'. Defaults to now if start is provided. - `--no-follow`: Historical only; do not stream live logs. - `--level TEXT`: Repeatable. –level ERROR –level WARN - `--source TEXT`: Filter by source/service. - `--plugin TEXT`: Repeatable. –plugin foo –plugin bar. - `--handler TEXT`: Repeatable. Qualified handler name (e.g. my_plugin.handlers.Foo). - `--page-size INTEGER`: Fetch size per page (historical). [default: 200] - `--limit INTEGER`: Max historical logs to print. - `--all`: Fetch all pages until exhausted (historical). - `--interactive`: After each page, prompt to load more. - `--cursor TEXT`: Resume token from a previous run. - `--help`: Show this message and exit. ### `canvas config list` List plugin variables on a Canvas instance. Each variable is rendered as `[set]` or `[not set]`, with a `(sensitive)` annotation for sensitive variables. Values themselves are never displayed — to read a value, use the Django Admin UI (gated by managing-user permissions). **Usage** : ```console $ canvas config list [OPTIONS] PLUGIN ``` **Example output** : ```console $ canvas config list my_plugin API_TOKEN [set] (sensitive) LOG_LEVEL [not set] ``` **Arguments** : - `PLUGIN`: Plugin name to list variables for **Options** : - `--host TEXT`: Canvas instance to connect to - `--help`: Show this message and exit. **Example Output** : ```console $ canvas config list my_plugin API_TOKEN = [set] (sensitive) WEBHOOK_URL = [set] DEBUG_MODE = [not set] ``` ### `canvas config set` Set (or update) one or more plugin variables. Pass one or more `KEY=value` pairs as positional arguments. For a plugin Canvas Platform manages, the values are stored in Canvas Platform and a configure deployment brings them to the running plugin on each `--instance`. Without `--instance`, the only instance the plugin is installed on that you can configure is targeted. Values are kept per instance, and a later `canvas deploy` carries them forward for the keys that revision declares; see Waiting for the outcome. For a plugin Canvas Platform does not manage, the values are written to the instance directly with `credentials.ini` (deprecated; see Instance API credentials). **Usage** : ```console $ canvas config set [OPTIONS] PLUGIN [KEY=VALUE]... ``` **Examples** : Set a single variable: ```console $ canvas config set acme__my_plugin API_TOKEN=your_api_token_value --instance acme-staging ``` Set multiple variables in one call: ```console $ canvas config set acme__my_plugin API_TOKEN=abc123 LOG_LEVEL=info --instance acme-staging ``` Set a variable whose value is a list with one entry per line — for example a redirect allowlist (see the [Redirect effect](/sdk/effect-redirect/)). The value is newline-delimited (not comma-separated), so use your shell's newline quoting to preserve the line breaks. In bash/zsh, ANSI-C quoting (`$'…'`) turns `\n` into a real newline: ```console $ canvas config set acme__my_plugin $'REDIRECT_ALLOWLIST_INTERNAL=/panel\n/patient' --instance acme-staging ``` **Arguments** : - `PLUGIN`: Plugin name to set variables for - `KEY=VALUE...`: Variables to set, keeping each one's declared sensitivity **Options** : - `--instance TEXT`: Instance slug to configure, repeatable - `--secret KEY=VALUE`: Set a sensitive value for a key no revision of the manifest declares, repeatable - `--variable KEY=VALUE`: Set a non-sensitive value for a key no revision of the manifest declares, repeatable - `--host TEXT`: Canvas instance to connect to, for a plugin Canvas Platform does not manage - `--help`: Show this message and exit. > Whether each value is treated as sensitive is determined by the plugin's `CANVAS_MANIFEST.json` (`variables: [{name, sensitive}]`) — `canvas config set` does not change the sensitive flag of a declared variable. ### `canvas config unset` Clear one or more plugin variables. For a plugin Canvas Platform manages, the values are cleared in Canvas Platform and a configure deployment removes them from the running plugin. For any other plugin, each value is set to empty on the instance directly. **Usage** : ```console $ canvas config unset [OPTIONS] PLUGIN KEY... ``` **Arguments** : - `PLUGIN`: Plugin name to clear variables for - `KEY...`: Variable keys to clear, e.g. `API_KEY` **Options** : - `--instance TEXT`: Instance slug to configure, repeatable - `--host TEXT`: Canvas instance to connect to, for a plugin Canvas Platform does not manage - `--help`: Show this message and exit. --- # Canvas Manifest Source: https://docs.canvasmedical.com/sdk/canvas_manifest/ Every plugin has a `CANVAS_MANIFEST.json` file at the root of its package. The manifest names the plugin, lists the handlers and applications Canvas loads, and declares the variables, URL permissions, and custom data namespace the plugin needs. The manifest is validated against a JSON schema when you run [`canvas validate`](/sdk/canvas_cli/#canvas-validate), or [`canvas validate-manifest`](/sdk/canvas_cli/#canvas-validate-manifest), and Canvas Platform checks it again on every [`canvas deploy`](/sdk/canvas_cli/#canvas-deploy) push. Unknown top-level keys and unknown component types fail validation. ## Where the manifest lives [`canvas init`](/sdk/canvas_cli/#canvas-init) creates a project with the plugin package inside it. The manifest sits at the top of the package directory, next to the plugin's code, and every path in the manifest (handler classes, icons, templates, the README) is relative to that directory. ```text my_plugin/ # project directory ├── pyproject.toml ├── tests/ │ └── test_event_handlers.py └── acme__my_plugin/ # plugin package ├── CANVAS_MANIFEST.json ├── OVERVIEW.md ├── README.md ├── __init__.py ├── assets/ │ └── icon.png ├── handlers/ │ ├── __init__.py │ └── event_handlers.py └── templates/ └── intake_form.yml ``` A handler in `acme__my_plugin/handlers/event_handlers.py` is referenced as `acme__my_plugin.handlers.event_handlers:ClassName`, and the icon as `assets/icon.png`. Pass the package directory, the one that holds `CANVAS_MANIFEST.json`, to `canvas validate` and `canvas deploy`: `canvas validate my_plugin/acme__my_plugin` from outside the project, or `canvas validate acme__my_plugin` from inside it. Canvas runs only the handlers and applications the manifest lists. A handler class in the package that the manifest doesn't reference is never loaded, and `canvas validate` warns about each one it finds. Every other file in the package directory still ships with the plugin, so your handlers can import your own modules and read templates and assets without listing them in the manifest. `canvas deploy` ships the files committed to the plugin's git repository, so `.gitignore` decides what is left out. See [Plugin repository layout](/sdk/canvas_cli/#canvas-deploy). ## Basic structure ```json { "sdk_version": "0.1.4", "plugin_version": "0.0.1", "name": "acme__my_plugin", "description": "A description of what this plugin does", "components": { "handlers": [], "applications": [], "commands": [], "questionnaires": [] }, "variables": [], "tags": {}, "references": [], "license": "", "diagram": false, "readme": "./README.md" } ``` ## Top-level fields Field| Type| Required| Description ---|---|---|--- `sdk_version`| string| Yes| The Canvas SDK version the plugin was built against. `plugin_version`| string| Yes| The plugin's version. Must not be empty. Increase it on every deploy. See Versioning your plugin. `name`| string| Yes| The plugin's name, which is also its package folder name. For a plugin deployed through Canvas Platform it is publisher-prefixed, `__` (for example `acme__intake`): lowercase letters, digits and underscores, starting with a letter on both sides of the double underscore. `canvas login` lists your organizations' prefixes. `description`| string| Yes| What the plugin does, in one sentence. The Canvas Platform catalog shows it under the plugin's title and on its card. `components`| object| Yes| The handlers, applications, commands, and questionnaires the plugin provides. Must contain at least one component type. See Components. `tags`| object| Yes| Categorization tags. Can be empty (`{}`). See Tags. `license`| string| Yes| A license identifier such as `"MIT"`, or an empty string. `readme`| string or boolean| Yes| Path to the plugin's README, or `false`. `variables`| array| No| Configuration values and secrets the plugin reads at runtime. See Variables. `secrets`| array| No| Deprecated. Use `variables` instead. `url_permissions`| array| No| External URLs the plugin's iframes may load, and what each may do. See URL permissions. `origins`| object| No| Legacy form of `url_permissions`. `custom_data`| object| No| The custom data namespace the plugin uses. See Custom data. `catalog`| object| No| The plugin's listing in the Canvas Platform plugin catalog. See Catalog. `references`| array of strings| No| Links to related documentation or resources. `diagram`| string or boolean| No| Path to an architecture or workflow diagram, or `false`. ### Versioning your plugin Change `plugin_version` every time you deploy a new build, including builds sent only to a test instance. Canvas accepts a redeploy with an unchanged version, so the version is the only way to tell which build an instance is running. It appears in: - the Plugins list in the Canvas Admin - `canvas list`, as `name@version` - the log lines written when the plugin is installed and loaded, for example `Successfully loaded plugin "acme__my_plugin", version 1.4.0` Use [semantic versioning](https://semver.org/) (`MAJOR.MINOR.PATCH`): Change| Bump| Example ---|---|--- A fix that doesn't change behavior users rely on| `PATCH`| `1.4.0` → `1.4.1` New functionality that leaves existing behavior in place| `MINOR`| `1.4.1` → `1.5.0` A change that breaks existing behavior, such as a renamed variable, a removed handler, or a changed custom data model| `MAJOR`| `1.5.0` → `2.0.0` #### Versioning with git `canvas deploy` deploys a commit pushed to the plugin's git repository on Canvas Platform, so every build on an instance maps to a commit. Tie each version to that history: - **Bump the version in the commit or pull request that changes the code.** Each merged change then carries its own version, and a reviewer can see what the new number ships. - **Tag the commit you release** (for example `git tag v1.5.0`, then `git push origin v1.5.0`). A version seen in the logs or the Admin then leads straight to the source that produced it, `git diff v1.4.1 v1.5.0` shows what changed between two releases, and `canvas deploy --ref v1.5.0 --instance ` deploys exactly that tag. To tell test builds apart without spending release numbers, add a pre-release suffix such as `1.5.0-rc.1` or `1.5.0-dev.3`, and drop the suffix for the build you release. ## Components The `components` object groups everything Canvas loads from your plugin. Each key holds a list, and the object must contain at least one key. Key| Holds ---|--- `handlers`| Classes that respond to Canvas events and serve plugin APIs. `applications`| Applications that appear in the Canvas UI. `commands`| Custom commands the plugin adds to notes. `questionnaires`| Questionnaire templates Canvas creates on install. ### Handlers Handlers respond to Canvas events: [event handlers](/sdk/handlers-basehandler/), [SimpleAPI](/sdk/handlers-simple-api/) endpoints, [CronTasks](/sdk/handlers-crontask/), [action buttons](/sdk/handlers-action-buttons/), [embedded applications](/sdk/handlers-embedded-applications/), and the rest of the [handler types](/sdk/handlers/). `protocols` is deprecated in favor of `handlers`. Canvas still loads classes listed under `protocols`, so existing plugins keep working, but new plugins should use `handlers`. To migrate, rename the key. The entries inside it stay the same. Field| Type| Required| Description ---|---|---|--- `class`| string| Yes| Import path to the handler class, in `package.module:ClassName` form. `description`| string| Yes| What the handler does. `meta`| object| No| Handler metadata. Used by clinical quality measure protocols. `data_access`| object| No| Accepted by the schema with `event`, `read`, and `write` keys, but not enforced. A handler's data access is not limited by this field. ```json { "components": { "handlers": [ { "class": "acme__my_plugin.handlers.patient_sync:PatientSync", "description": "Sends patient updates to an external system" } ] } } ``` ### Applications Applications add entry points to the Canvas UI. See [Applications](/sdk/handlers-applications/) for where each one appears and the handler that runs when a user opens it. Field| Type| Required| Description ---|---|---|--- `class`| string| Yes| Import path to the `Application` subclass, in `package.module:ClassName` form. `name`| string| Yes| Display name. Up to 32 characters. `description`| string| Yes| What the application does. Up to 256 characters. `icon`| string| Yes| Path to an image inside the plugin package, or an `https://` URL to one. Rendered at 48 × 48 px. `scope`| string| Yes| Where the application appears. See [Application Scopes](/sdk/handlers-applications/#application-scopes) for the values. `menu_position`| string| No| `"top"` or `"bottom"`. Defaults to `"top"`. `menu_order`| integer| No| Order of the application within its menu position. Lower numbers appear first. `show_in_panel`| boolean| No| Shows the application alongside the panel buttons instead of in the app drawer. Defaults to `false`. `panel_priority`| integer| No| Order of the application among panel applications. ```json { "components": { "applications": [ { "class": "acme__my_plugin.applications.risk_calculator:RiskCalculator", "name": "Risk Calculator", "description": "Calculates cardiovascular risk for the open patient", "icon": "assets/risk_calculator.png", "scope": "patient_specific", "show_in_panel": true, "panel_priority": 100 } ] } } ``` Embedded applications (note applications, scheduling applications, and docked applications) are declared under `handlers`, not `applications`, and take no `scope` or `icon`. See [Embedded Applications](/sdk/handlers-embedded-applications/). ### Commands The `commands` list declares [custom commands](/sdk/commands-custom-command/): commands with plugin-rendered HTML content that can be added to a note. Field| Type| Required| Description ---|---|---|--- `name`| string| Yes| Unique name for the command. `schema_key`| string| Yes| Identifier the `CustomCommand` effect uses to refer to this command. Must be unique across every plugin installed on the instance, or installation fails. `label`| string| No| Label shown in the Canvas UI. `section`| string| No| Note section the command belongs to: `subjective`, `objective`, `assessment`, `plan`, `procedures`, `history`, or `internal`. ```json { "components": { "commands": [ { "name": "RiskAssessment", "label": "Risk Assessment", "schema_key": "myPluginRiskAssessment", "section": "assessment" } ] } } ``` ### Questionnaires The `questionnaires` list points at YAML templates in the plugin package. Canvas creates each questionnaire when the plugin is installed. See [Questionnaires](/sdk/effect-questionnaires/#manifest-yaml-template) for the template format. Field| Type| Required| Description ---|---|---|--- `template`| string| Yes| Path to the questionnaire's YAML template inside the plugin package. ```json { "components": { "questionnaires": [ { "template": "templates/intake_form.yml" } ] } } ``` ### Content, effects, and views `content`, `effects`, and `views` are accepted for compatibility and have no effect. `canvas init` adds them as empty lists, and you can remove them. ## Variables `variables` declares the configuration values and secrets a plugin reads from `self.secrets` at runtime. Values are set per instance with [`canvas config set`](/sdk/canvas_cli/#canvas-config-set). See [Managing Variables](/sdk/secrets/). `namespace_read_access_key` and `namespace_read_write_access_key` are reserved for a plugin deployed through Canvas Platform: Canvas Platform supplies them to plugins entitled to a custom data namespace, and refuses a manifest that declares them. Field| Type| Required| Description ---|---|---|--- `name`| string| Yes| The key the value is read under in `self.secrets`. `sensitive`| boolean| No| When `true`, the value is hidden in the Admin UI and CLI listings. Defaults to `false`. `default`| string| No| Accepted by the schema for non-sensitive variables only. Canvas does not pre-fill the variable with it, so set the value with `canvas config set`. ```json { "variables": [ {"name": "API_TOKEN", "sensitive": true}, {"name": "API_BASE_URL"} ] } ``` The older `secrets` field is a list of names, each treated as a non-sensitive variable. `canvas validate-manifest` prints a deprecation warning when a manifest uses it. ```json { "secrets": ["API_TOKEN"] } ``` ## URL permissions A [`LaunchModalEffect` or `PortalWidget`](/sdk/layout-effect/#additional-configuration) only loads URLs and external scripts listed in `url_permissions`. Each entry names a URL and the iframe capabilities it is granted. Field| Type| Required| Description ---|---|---|--- `url`| string| Yes| The URL to allow, in [CSP host-source](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#host-source) format. `permissions`| array of strings| Yes| Capabilities granted to the URL. Can be empty. Permission| Grants ---|--- `SCRIPTS`| JavaScript execution `ALLOW_SAME_ORIGIN`| Same-origin access `MICROPHONE`| Microphone access `CAMERA`| Camera access `CLIPBOARD_READ`| Reading from the clipboard `CLIPBOARD_WRITE`| Writing to the clipboard ```json { "url_permissions": [ { "url": "https://example.com", "permissions": ["SCRIPTS", "ALLOW_SAME_ORIGIN", "MICROPHONE"] } ] } ``` See [Additional Configuration](/sdk/layout-effect/#additional-configuration) on the layout effects page for when each permission is needed. ### Origins (legacy) `origins` is the older form of `url_permissions`. A manifest can use one or the other, not both. Canvas converts each `urls` entry to a URL with no permissions and each `scripts` entry to a URL with `SCRIPTS`. ```json { "origins": { "urls": ["https://example.com"], "scripts": ["https://cdn.example.com"] } } ``` ## Custom data `custom_data` declares the namespace a plugin stores [custom data](/sdk/custom-data/) in, and whether it can write to it. See the [Custom Data Quick Start](/sdk/custom-data-quick-start/). Field| Type| Required| Description ---|---|---|--- `namespace`| string| Yes| Namespace name in `org__name` form: lowercase letters, digits, and underscores, with a double underscore between the two parts. Up to 63 characters. `access`| string| Yes| `"read"` for read-only access, or `"read_write"`. ```json { "custom_data": { "namespace": "my_org__my_plugin", "access": "read_write" } } ``` ## Catalog `catalog` is the plugin's listing in the Canvas Platform plugin catalog. Canvas Platform reads it from the manifest each time a push moves the plugin's `main` branch, so the listing ships with the code it describes; pushes to other branches and tags leave the catalog as it is. It refuses a push whose listing breaks the rules below. [`canvas deploy --push-only`](/sdk/canvas_cli/#canvas-deploy) updates the listing without deploying to an instance, and `canvas validate` checks it locally. ```json { "catalog": { "title": "Intake Summary", "kind": "plugin", "category": "Charting", "surfaces": ["Note"], "keywords": ["intake", "summary"], "screenshots": [ {"path": "assets/summary.png", "caption": "In the note", "alt": "An intake summary in a note"} ], "pricing": [ {"unit": "visit", "price_per_unit": "1.50"}, {"unit": "token"} ], "vendors": [ {"name": "Twilio", "relationship": "self_managed", "additional_fees": true} ], "icon": "assets/icon.png", "setup_guide": "setup_guide.md", "setup_instructions": "setup_instructions.md", "release_notes": {"kind": "fix", "title": "Summaries keep their line breaks"} } } ``` Field| Type| Required| Description ---|---|---|--- `title`| string| Yes| The name on the plugin's card and page. At most 64 characters. `category`| string| Yes| One of `Billing & RCM`, `Charting`, `Decision support`, `Interoperability`, `Labs & devices`, `Operations`, `Patient engagement`, `Population health`, `Prescribing`, `Scheduling`. `surfaces`| array| Yes| Every place in Canvas the plugin's work shows up, at least one and none repeated: `Note`, `Chart app`, `Command`, `Background`, `Patient portal`, `Waffle`. `kind`| string| No| `agent` for a plugin that calls a model and acts with latitude, or `plugin` for a deterministic one. Defaults to `plugin`. `agent`| object| When `kind` is `agent`| The agent's boundary: `does`, `does_not`, `runs_when` (text) and `models` (at least one model name). A `plugin` must leave it out. `keywords`| array| No| Free search terms, separate from Tags: lowercase letters, digits and hyphens, up to 32 characters each. `screenshots`| array| No| Up to eight images, in display order. Each has a `path` ending in `.png`, `.jpg`, `.jpeg` or `.webp` in lowercase, a required `alt` of at most 200 characters, and an optional `caption` of at most 40. `pricing`| array| No| What Canvas charges, up to 10 components in the order the plugin's page lists them. Each has a `unit` in the singular (at most 40 characters, named once) and a `price_per_unit`: a string of US dollars such as `"5.75"`, with at most four decimal places. The `token` unit takes no price, because the inference gateway prices tokens by model. Leave `pricing` out for a plugin with no charge. `vendors`| array| No| The third parties the plugin depends on, up to 10. Each has a `name` (at most 64 characters, named once), a `relationship` of `canvas_managed` (Canvas sets up the account) or `self_managed` (the customer does), and `additional_fees`: `true` when the vendor bills the customer directly for its own fees, separately from Canvas. `icon`| string| No| Path to the image shown beside the title on the plugin's card, ending in `.png`, `.jpg`, `.jpeg` or `.webp` in lowercase. `setup_guide`| string| No| Path to a markdown file describing setup for the person deciding whether to install the plugin: what to have ready, the steps, how long it takes. The plugin's page shows it under How setup works. `setup_instructions`| string| No| Path to a file that Studio's agent reads when an organization installs the plugin. It is not shown on the plugin's page. `release_notes`| object| No| What changed in this `plugin_version`: `kind` (`fix`, `performance` or `breaking`), `title`, and an optional `body`. Canvas Platform assembles the history from every pushed revision. Every path is inside the package folder: no leading `/`, no `..` anywhere in it, and no backslash. Unknown fields are refused at every level, and optional fields are left out rather than set to `null`. Someone holding the Publisher role in the plugin's organization can also edit the listing from the plugin's page in the Canvas Platform catalog, including the `description`. Saving there is a commit to `main` with the listing changed and a patch bump of `plugin_version`, so pull before your next push. The edit is refused unless `plugin_version` on `main` is a plain `MAJOR.MINOR.PATCH` version, so a pre-release suffix such as `1.5.0-rc.1` blocks it. An `OVERVIEW.md` file beside `CANVAS_MANIFEST.json` in the package folder is the plugin's overview, and needs no field in `catalog`. When it is there, the plugin's page shows it as rendered markdown beside the plugin's pricing and setup. `canvas init` creates one to start from. ## Tags Tags categorize a plugin. Unrecognized tag categories and values produce warnings during validation rather than errors. Category| Allowed values ---|--- `patient_sourcing_and_intake`| `symptom_triage`, `coverage_capture` `interaction_modes_and_utilization`| `supply_policies`, `demand_policies`, `auto_followup` `content`| `patient_intake` `diagnostic_range_and_inputs`| None yet. Use an empty list. `pricing_and_payments`| None yet. Use an empty list. `care_team_composition`| None yet. Use an empty list. `interventions_and_safety`| None yet. Use an empty list. ```json { "tags": { "patient_sourcing_and_intake": ["symptom_triage"] } } ``` ## Complete example A plugin with a patient chart application, the SimpleAPI handler that serves it, a custom command, and a sensitive variable: ```json { "sdk_version": "0.1.4", "plugin_version": "1.0.0", "name": "risk_calculator", "description": "Calculates clinical risk scores for patients", "url_permissions": [ { "url": "https://risk.example.com", "permissions": ["SCRIPTS", "ALLOW_SAME_ORIGIN"] } ], "components": { "applications": [ { "class": "risk_calculator.applications.calculator:RiskCalculatorApp", "name": "Risk Calculator", "description": "Calculates cardiovascular risk for the open patient", "icon": "assets/risk_calculator.png", "scope": "patient_specific", "show_in_panel": true, "panel_priority": 100 } ], "handlers": [ { "class": "risk_calculator.handlers.api:RiskCalculatorAPI", "description": "Serves the calculator page and its data" } ], "commands": [ { "name": "RiskAssessment", "label": "Risk Assessment", "schema_key": "riskCalculatorAssessment", "section": "assessment" } ] }, "variables": [ {"name": "RISK_API_TOKEN", "sensitive": true} ], "tags": {}, "references": [], "license": "MIT", "diagram": false, "readme": "./README.md" } ``` ## Validation Validate a manifest with the [Canvas CLI](/sdk/canvas_cli/): ```bash canvas validate acme__my_plugin ``` `canvas validate` checks the manifest against the schema and then loads every handler in the plugin sandbox, which catches disallowed imports and other errors that only appear on the instance. `canvas validate-manifest my_plugin` runs the manifest checks alone. --- # AWS S3 Source: https://docs.canvasmedical.com/sdk/clients-aws-s3/ The Canvas SDK AWS S3 client provides a simple interface for interacting with Amazon S3 storage, including uploading, downloading, listing, and deleting objects, as well as generating presigned URLs for temporary access. ## Requirements - **AWS Access Key ID** : Your AWS access key - **AWS Secret Access Key** : Your AWS secret key - **AWS Region** : The region where your bucket is located (e.g., `us-east-1`) - **S3 Bucket Name** : The name of your S3 bucket ## Imports The AWS S3 client is included in the Canvas SDK. Import the necessary components: ```python from canvas_sdk.clients.aws import S3, Credentials, S3Item ``` Or import from specific modules: ```python from canvas_sdk.clients.aws.libraries import S3 from canvas_sdk.clients.aws.structures import Credentials, S3Item ``` ## Initialize the Client ```python from canvas_sdk.clients.aws import S3, Credentials credentials = Credentials( key="your_aws_access_key_id", secret="your_aws_secret_access_key", region="us-east-1", bucket="your-bucket-name" ) client = S3(credentials) ``` ## Check if Client is Ready ```python if client.is_ready(): print("S3 client is configured and ready") else: print("Missing credentials") ``` ## Upload a Text File ```python from canvas_sdk.clients.aws import S3, Credentials credentials = Credentials( key="your_access_key", secret="your_secret_key", region="us-east-1", bucket="my-bucket" ) client = S3(credentials) # Upload text content response = client.upload_text_to_s3("path/to/file.txt", "Hello, World!") if response and response.status_code == 200: print("Text file uploaded successfully!") ``` ## Upload a Binary File ```python # Upload binary content (e.g., an image) with open("local_image.png", "rb") as f: binary_data = f.read() response = client.upload_binary_to_s3( "images/uploaded_image.png", binary_data, "image/png" ) if response and response.status_code == 200: print("Binary file uploaded successfully!") ``` ## Download a File ```python response = client.access_s3_object("path/to/file.txt") if response: content = response.content print(f"Downloaded content: {content.decode('utf-8')}") ``` ## List Objects in Bucket ```python # List all objects with a prefix items = client.list_s3_objects("documents/") if items: for item in items: print(f"Key: {item.key}, Size: {item.size} bytes, Modified: {item.last_modified}") ``` ## Delete an Object ```python response = client.delete_object("path/to/file.txt") if response and response.status_code == 204: print("Object deleted successfully!") ``` ## Generate a Presigned URL ```python # Generate a URL valid for 1 hour (3600 seconds) url = client.generate_presigned_url("path/to/file.txt", expiration=3600) if url: print(f"Presigned URL: {url}") ``` ## S3 The main class for interacting with AWS S3. ### Constructor ```python S3(credentials: Credentials) ``` Parameter| Type| Description ---|---|--- `credentials`| `Credentials`| AWS credentials for S3 access ### Methods #### `is_ready() -> bool` Check if all required credentials are provided. **Returns:** `True` if all credentials (key, secret, region, bucket) are non-empty, `False` otherwise. #### `access_s3_object(object_key: str) -> Response | None` Download an object from S3. **Parameters:** Parameter| Type| Description ---|---|--- `object_key`| `str`| S3 object key (path) to access **Returns:** `requests.Response` containing the object data, or `None` if credentials are not ready. #### `upload_text_to_s3(object_key: str, data: str) -> Response | None` Upload text data to S3 as `text/plain`. **Parameters:** Parameter| Type| Description ---|---|--- `object_key`| `str`| S3 object key (path) to create/update `data`| `str`| Text content to upload **Returns:** `requests.Response` from S3, or `None` if credentials are not ready. #### `upload_binary_to_s3(object_key: str, binary_data: bytes, content_type: str) -> Response | None` Upload binary data to S3. **Parameters:** Parameter| Type| Description ---|---|--- `object_key`| `str`| S3 object key (path) to create/update `binary_data`| `bytes`| Binary content to upload `content_type`| `str`| MIME type (e.g., `image/png`) **Returns:** `requests.Response` from S3, or `None` if credentials are not ready. #### `delete_object(object_key: str) -> Response | None` Delete an object from S3. **Parameters:** Parameter| Type| Description ---|---|--- `object_key`| `str`| S3 object key (path) to delete **Returns:** `requests.Response` from S3, or `None` if credentials are not ready. #### `list_s3_objects(prefix: str) -> list[S3Item] | None` List all objects in S3 with the given prefix. Handles pagination automatically. **Parameters:** Parameter| Type| Description ---|---|--- `prefix`| `str`| S3 key prefix to filter objects **Returns:** List of `S3Item` objects with metadata, or `None` if credentials are not ready. **Raises:** `Exception` if S3 returns a non-200 status code. #### `generate_presigned_url(object_key: str, expiration: int) -> str | None` Generate a presigned URL for temporary access to an S3 object. **Parameters:** Parameter| Type| Description ---|---|--- `object_key`| `str`| S3 object key (path) `expiration`| `int`| URL expiration time in seconds **Returns:** Presigned URL string, or `None` if credentials are not ready. ## Data Structures ### Credentials AWS credentials for S3 access. Field| Type| Description ---|---|--- `key`| `str`| AWS access key ID `secret`| `str`| AWS secret access key `region`| `str`| AWS region (e.g., `us-east-1`) `bucket`| `str`| S3 bucket name **Example:** ```python from canvas_sdk.clients.aws import Credentials credentials = Credentials( key="AKIAIOSFODNN7EXAMPLE", secret="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", region="us-west-2", bucket="my-application-bucket" ) ``` ### S3Item S3 object metadata returned by `list_s3_objects`. Field| Type| Description ---|---|--- `key`| `str`| Object key (path) in the S3 bucket `size`| `int`| Object size in bytes `last_modified`| `datetime`| Timestamp of the last modification **Example:** ```python items = client.list_s3_objects("documents/") for item in items: print(f"File: {item.key}") print(f"Size: {item.size} bytes") print(f"Last Modified: {item.last_modified}") ``` ## Complete Plugin Example Here's a complete example of using the S3 client in a Canvas plugin: ```python from http import HTTPStatus from canvas_sdk.clients.aws import S3, Credentials from canvas_sdk.effects import Effect from canvas_sdk.effects.simple_api import JSONResponse, PlainTextResponse, Response from canvas_sdk.handlers.simple_api import Credentials as APICredentials, SimpleAPI, api class S3Handler(SimpleAPI): """Simple API handler for S3 operations.""" def authenticate(self, credentials: APICredentials) -> bool: return True def _s3_client(self) -> S3: """Create S3 client from plugin secrets.""" return S3( Credentials( key=self.secrets["S3Key"], secret=self.secrets["S3Secret"], region=self.secrets["S3Region"], bucket=self.secrets["S3Bucket"], ) ) @api.get("/list") def list_files(self) -> list[Response | Effect]: """List all files in the bucket.""" client = self._s3_client() if client.is_ready(): items = client.list_s3_objects("") content = [{"key": p.key, "size": p.size} for p in items] return [JSONResponse(content, status_code=HTTPStatus.OK)] return [] @api.get("/download/") def download_file(self) -> list[Response | Effect]: """Download a file by key.""" file_key = self.request.path_params["file_key"] client = self._s3_client() if client.is_ready() and file_key: response = client.access_s3_object(file_key) return [Response(response.content, status_code=HTTPStatus.OK)] return [] @api.post("/upload/") def upload_file(self) -> list[Response | Effect]: """Upload a file.""" file_key = self.request.path_params["file_key"] client = self._s3_client() content = self.request.body content_type = self.request.content_type if client.is_ready() and file_key: if content_type == "text/plain": response = client.upload_text_to_s3(file_key, content.decode("utf-8")) else: response = client.upload_binary_to_s3(file_key, content, content_type) return [Response(response.content, status_code=response.status_code)] return [] @api.delete("/delete/") def delete_file(self) -> list[Response | Effect]: """Delete a file by key.""" file_key = self.request.path_params["file_key"] client = self._s3_client() if client.is_ready() and file_key: response = client.delete_object(file_key) return [Response(response.content, status_code=HTTPStatus.OK)] return [] @api.get("/presigned/") def get_presigned_url(self) -> list[Response | Effect]: """Generate a presigned URL for temporary access.""" file_key = self.request.path_params["file_key"] client = self._s3_client() if client.is_ready() and file_key: url = client.generate_presigned_url(file_key, 3600) # 1 hour return [PlainTextResponse(url, status_code=HTTPStatus.OK)] return [] ``` ## Error Handling The S3 client methods return `None` when credentials are not ready. For list operations, an `Exception` is raised if S3 returns an error status code. ```python # Check credentials before operations if not client.is_ready(): print("S3 credentials are not configured") return # Handle list errors try: items = client.list_s3_objects("prefix/") except Exception as e: print(f"S3 error: {e}") # Check response status for uploads/downloads response = client.upload_text_to_s3("file.txt", "content") if response: if response.status_code == 200: print("Upload successful") else: print(f"Upload failed with status {response.status_code}") else: print("Credentials not ready") ``` ## AWS Signature V4 Authentication The S3 client implements AWS Signature Version 4 for request authentication. This is handled automatically - you only need to provide valid credentials. The client: - Signs all requests with HMAC-SHA256 - Generates proper canonical requests - Handles date/time formatting for AWS - Supports presigned URLs for temporary access ## Additional Resources - [AWS S3 Documentation](https://docs.aws.amazon.com/s3/) - [AWS Signature Version 4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html) - [S3 REST API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html) - [Example Plugin](/sdk/example-aws_s3/) \- Documentation for the example plugin - [Source Code](https://github.com/canvas-medical/canvas-plugins/tree/main/example-plugins/aws_s3) \- View the source on GitHub --- # Canvas FHIR Source: https://docs.canvasmedical.com/sdk/clients-canvas-fhir/ The Canvas SDK FHIR client provides a simple interface for interacting with the [Canvas FHIR API](/api/), supporting CRUD operations on FHIR resources such as Coverages, DocumentReferences, AllergyIntolerances, and more. It handles OAuth client credentials authentication and token caching automatically. ## Requirements - **Canvas FHIR Client ID** : An OAuth client ID for your Canvas environment - **Canvas FHIR Client Secret** : The corresponding OAuth client secret These credentials should be stored as [plugin secrets](/sdk/secrets/) and grant access to the Canvas FHIR API for your environment. ## Imports The Canvas FHIR client is included in the Canvas SDK. Import the client: ```python from canvas_sdk.clients.canvas_fhir import CanvasFhir ``` ## Initialize the Client ```python # Declare these secrets in the CANVAS_MANIFEST.json and set the values on the # plugin configuration page. client_id = self.secrets["CANVAS_FHIR_CLIENT_ID"] client_secret = self.secrets["CANVAS_FHIR_CLIENT_SECRET"] client = CanvasFhir(client_id, client_secret) ``` On initialization, the client will: 1. Authenticate using the OAuth client credentials flow against your Canvas environment's token endpoint. 2. Cache the access token using the plugin cache system, keyed by `client_id`, with automatic expiration. 3. Determine the FHIR API base URL from the environment's `CUSTOMER_IDENTIFIER` setting (e.g., `https://fumage-{CUSTOMER_IDENTIFIER}.canvasmedical.com`). ## CanvasFhir The main class for interacting with the Canvas FHIR API. ### Constructor ```python CanvasFhir(client_id: str, client_secret: str) ``` Parameter| Type| Description ---|---|--- `client_id`| `str`| OAuth client ID for the Canvas API `client_secret`| `str`| OAuth client secret ### Methods #### `search(resource_type: str, parameters: dict) -> dict` Search for FHIR resources matching the given parameters. ```python # Search for a patient's allergy intolerances results = client.search("AllergyIntolerance", {"patient": "Patient/abc123"}) for entry in results.get("entry", []): resource = entry["resource"] print(f"Allergy: {resource['code']['coding'][0]['display']}") ``` Parameter| Type| Description ---|---|--- `resource_type`| `str`| FHIR resource type (e.g., `Patient`, `Coverage`) `parameters`| `dict`| Search parameters as key-value pairs **Returns:** FHIR Bundle `dict` containing matching resources. **Raises:** `requests.HTTPError` if the API returns an error status code. #### `read(resource_type: str, resource_id: str) -> dict` Read a single FHIR resource by its ID. ```python # Read a specific resource by ID allergy = client.read("AllergyIntolerance", "allergy-id-123") print(f"Status: {allergy['clinicalStatus']['coding'][0]['code']}") ``` Parameter| Type| Description ---|---|--- `resource_type`| `str`| FHIR resource type `resource_id`| `str`| ID of the resource to read **Returns:** FHIR resource `dict`. **Raises:** `requests.HTTPError` if the API returns an error status code. #### `create(resource_type: str, data: dict) -> dict` Create a new FHIR resource. ```python # Create a new Coverage resource coverage = client.create("Coverage", { "resourceType": "Coverage", "status": "active", "beneficiary": {"reference": "Patient/abc123"}, "payor": [{"reference": "Organization/org-456"}], }) print(f"Created Coverage: {coverage['id']}") ``` Parameter| Type| Description ---|---|--- `resource_type`| `str`| FHIR resource type `data`| `dict`| FHIR resource data to create **Returns:** Created FHIR resource `dict` (including server-assigned `id`). **Raises:** `requests.HTTPError` if the API returns an error status code. #### `update(resource_type: str, resource_id: str, data: dict) -> dict` Update an existing FHIR resource. ```python # Update an existing resource updated = client.update("Coverage", "coverage-id-789", { "resourceType": "Coverage", "id": "coverage-id-789", "status": "cancelled", "beneficiary": {"reference": "Patient/abc123"}, "payor": [{"reference": "Organization/org-456"}], }) print(f"Updated Coverage status: {updated['status']}") ``` Parameter| Type| Description ---|---|--- `resource_type`| `str`| FHIR resource type `resource_id`| `str`| ID of the resource to update `data`| `dict`| Complete FHIR resource data **Returns:** Updated FHIR resource `dict`. **Raises:** `requests.HTTPError` if the API returns an error status code. ### Sending other requests The methods above parse every response as JSON. When you need a request they don't cover, or the raw response, send it yourself with the [`Http`](/sdk/utils/) util. Build the request from the client's `base_url` and `get_headers()`: ```python from canvas_sdk.utils import Http response = Http().post( f"{client.base_url}/Coverage", headers=client.get_headers(), json=coverage, ) response.raise_for_status() # The raw response is yours to read, including an empty body. result = response.json() if response.text.strip() else {} ``` Member| Type| Description ---|---|--- `base_url`| `str`| The FHIR API base URL for your environment, such as `https://fumage-{CUSTOMER_IDENTIFIER}.canvasmedical.com`. `get_headers()`| `dict`| The headers a FHIR API request needs. These are a bearer `Authorization` header carrying the client's access token, and `Content-Type: application/json`. ## Authentication The client uses the OAuth 2.0 client credentials flow to authenticate with the Canvas API. Token management is handled automatically: - On first use, the client exchanges the `client_id` and `client_secret` for an access token via the Canvas token endpoint. - The token is cached using the plugin cache system with the key `canvas_fhir_credentials_{client_id}`. - The cached token expires 60 seconds before the actual token expiration to avoid using stale credentials. - Subsequent requests reuse the cached token until it expires. ## Error Handling The Canvas FHIR client uses `raise_for_status()` on all HTTP responses, which raises `requests.HTTPError` for non-successful status codes. ```python from requests import HTTPError try: result = client.read("Patient", "nonexistent-id") except HTTPError as e: print(f"HTTP {e.response.status_code}: {e.response.text}") ``` ## Complete Plugin Example Here's a complete example of using the Canvas FHIR client in an ActionButton handler: ```python from canvas_sdk.clients.canvas_fhir import CanvasFhir from canvas_sdk.effects import Effect from canvas_sdk.handlers.action_button import ActionButton from logger import log class FhirRequestHandler(ActionButton): """Handler that queries the FHIR API when a button is clicked.""" BUTTON_TITLE = "Trigger FHIR Request" BUTTON_KEY = "TRIGGER_FHIR_REQUEST" BUTTON_LOCATION = ActionButton.ButtonLocation.CHART_SUMMARY_ALLERGIES_SECTION def handle(self) -> list[Effect]: """Handle the button click.""" client_id = self.secrets["CANVAS_FHIR_CLIENT_ID"] client_secret = self.secrets["CANVAS_FHIR_CLIENT_SECRET"] patient_id = self.event.target.id client = CanvasFhir(client_id, client_secret) # Search for the patient's allergy intolerances search_response = client.search( "AllergyIntolerance", {"patient": f"Patient/{patient_id}"}, ) log.info(f"Search: {search_response}") # Read the first result first_entry = search_response["entry"][0]["resource"] read_response = client.read("AllergyIntolerance", first_entry["id"]) log.info(f"Read: {read_response}") return [] ``` ## Additional Resources - [Canvas FHIR API Documentation](/api/) - [Example Plugin Source Code](https://github.com/canvas-medical/canvas-plugins/tree/main/example-plugins/canvas_fhir_client) --- # Extend AI Source: https://docs.canvasmedical.com/sdk/clients-extend-ai/ The Canvas SDK Extend AI client provides an interface for document processing using AI-powered extraction, classification, and splitting capabilities through the Extend AI API. ## Requirements - **Extend AI API Key** : Obtain from your [Extend AI dashboard](https://app.extend.ai/) ## What is Extend AI? Extend AI provides intelligent document processing (IDP) capabilities: - **Extraction** : Extract structured data from documents based on a defined schema - **Classification** : Classify documents into predefined categories - **Splitting** : Split multi-page documents into logical sections ## Imports The Extend AI client is included in the Canvas SDK. Import the necessary components: ```python from canvas_sdk.clients.extend_ai.libraries import Client from canvas_sdk.clients.extend_ai.constants import RunStatus, VersionName from canvas_sdk.clients.extend_ai.structures import RequestFailed ``` ## Initialize the Client ```python client = Client(key="your_extend_ai_api_key") ``` ## Extract Data from a Document (Using an Existing Processor) The most common use case is running an existing processor on a document. Here's a complete example: ```python import time from canvas_sdk.clients.extend_ai.libraries import Client from canvas_sdk.clients.extend_ai.constants import RunStatus from canvas_sdk.clients.extend_ai.structures import RequestFailed # Initialize the client client = Client(key="your_api_key") # Your processor ID (created in the Extend AI dashboard) processor_id = "proc_xxxxxxxxxxxxxxxxx" # URL to the document (must be publicly accessible) document_url = "https://your-bucket.s3.amazonaws.com/document.pdf" try: # Start the processor run run = client.run_processor( processor_id=processor_id, file_name="my-document.pdf", file_url=document_url, config=None, # Use processor's default configuration ) print(f"Run started! ID: {run.id}, Status: {run.status.value}") # Poll for completion while run.status in (RunStatus.PENDING, RunStatus.PROCESSING): time.sleep(2) # Wait 2 seconds between checks run = client.run_status(run.id) print(f"Status: {run.status.value}") # Check result if run.status == RunStatus.PROCESSED: print("Extraction successful!") print(f"Extracted data: {run.output.value}") else: print(f"Processing failed with status: {run.status.value}") except RequestFailed as e: print(f"Error: {e.message} (HTTP {e.status_code})") ``` ## List Available Processors ```python # List all processors in your account for processor in client.list_processors(): print(f"ID: {processor.id}") print(f"Name: {processor.name}") print(f"Type: {processor.type.value}") print("---") ``` ## Get Processor Configuration ```python from canvas_sdk.clients.extend_ai.constants import VersionName # Get the draft version of a processor processor_version = client.processor( processor_id="proc_xxxxxxxxxxxxxxxxx", version=VersionName.DRAFT.value ) print(f"Processor: {processor_version.processor.name}") print(f"Version: {processor_version.version}") print(f"Type: {processor_version.processor.type.value}") # Access the schema (for extraction processors) if hasattr(processor_version.config, 'schema'): print(f"Schema: {processor_version.config.schema}") ``` ## Check Run Status and Get Results ```python # Check the status of a run run = client.run_status("run_xxxxxxxxxxxxxxxxx") print(f"Status: {run.status.value}") print(f"Credits used: {run.usage}") if run.status == RunStatus.PROCESSED: # For extraction processors if hasattr(run.output, 'value'): extracted_data = run.output.value print(f"Extracted: {extracted_data}") # For classification processors if hasattr(run.output, 'type'): print(f"Classification: {run.output.type}") print(f"Confidence: {run.output.confidence}") # For splitter processors if hasattr(run.output, 'splits'): for split in run.output.splits: print(f"Split: {split.type}, Pages {split.startPage}-{split.endPage}") ``` ## Clean Up Files After Processing ```python # After processing, delete the uploaded files to save storage run = client.run_status("run_xxxxxxxxxxxxxxxxx") if run.status == RunStatus.PROCESSED: for file in run.files: deleted = client.delete_file(file.id) print(f"Deleted file {file.name}: {deleted}") ``` ## Complete Workflow Example ```python import time from canvas_sdk.clients.extend_ai.libraries import Client from canvas_sdk.clients.extend_ai.constants import RunStatus from canvas_sdk.clients.extend_ai.structures import RequestFailed def extract_from_document(api_key: str, processor_id: str, document_url: str) -> dict: """ Extract structured data from a document using Extend AI. Args: api_key: Your Extend AI API key processor_id: The processor ID to use document_url: Public URL to the document Returns: Dictionary containing the extracted data Raises: RequestFailed: If the API request fails RuntimeError: If processing fails or times out """ client = Client(key=api_key) # Start processing run = client.run_processor( processor_id=processor_id, file_name="document.pdf", file_url=document_url, config=None, ) # Wait for completion (with timeout) max_attempts = 30 # 60 seconds max attempts = 0 while run.status in (RunStatus.PENDING, RunStatus.PROCESSING): if attempts >= max_attempts: raise RuntimeError("Processing timed out") time.sleep(2) run = client.run_status(run.id) attempts += 1 # Handle result if run.status == RunStatus.PROCESSED: # Clean up files for file in run.files: client.delete_file(file.id) return run.output.value if hasattr(run.output, 'value') else run.output.to_dict() raise RuntimeError(f"Processing failed: {run.status.value}") # Usage result = extract_from_document( api_key="your_api_key", processor_id="proc_xxxxxxxxxxxxxxxxx", document_url="https://example.com/document.pdf" ) print(result) ``` ## Client The main class for interacting with the Extend AI API. ### Constructor ```python Client(key: str) ``` Parameter| Type| Description ---|---|--- `key`| `str`| Extend AI API key ### File Management #### `list_files() -> Iterator[StoredFile]` List all files stored in Extend AI. ```python for file in client.list_files(): print(f"{file.id}: {file.name} ({file.type})") ``` **Returns:** Iterator of `StoredFile` objects **Raises:** `RequestFailed` on error #### `delete_file(file_id: str) -> bool` Delete a file from Extend AI storage. ```python deleted = client.delete_file("file_xxxxxxxxxxxxxxxxx") print(f"Deleted: {deleted}") ``` Parameter| Type| Description ---|---|--- `file_id`| `str`| Unique identifier of the file **Returns:** `True` on success **Raises:** `RequestFailed` on error ### Processor Management #### `list_processors() -> Iterator[ProcessorMeta]` List all processors in the account. ```python for processor in client.list_processors(): print(f"{processor.name}: {processor.type.value}") ``` **Returns:** Iterator of `ProcessorMeta` objects **Raises:** `RequestFailed` on error #### `processor(processor_id: str, version: str) -> ProcessorVersion` Get details for a specific processor version. ```python from canvas_sdk.clients.extend_ai.constants import VersionName # Get draft version processor = client.processor("proc_xxx", VersionName.DRAFT.value) # Get latest published version processor = client.processor("proc_xxx", VersionName.LATEST.value) # Get specific version processor = client.processor("proc_xxx", "v1") ``` Parameter| Type| Description ---|---|--- `processor_id`| `str`| Unique identifier of the processor `version`| `str`| Version name (`draft`, `latest`, or `vN`) **Returns:** `ProcessorVersion` object **Raises:** `RequestFailed` on error #### `create_processor(name: str, config: ConfigBase) -> ProcessorMeta` Create a new processor with the specified configuration. ```python from canvas_sdk.clients.extend_ai.constants import BaseProcessor from canvas_sdk.clients.extend_ai.structures.config import ( ConfigExtraction, AdvancedOptionsExtraction, Parser, ) config = ConfigExtraction( base_processor=BaseProcessor.EXTRACTION_PERFORMANCE, extraction_rule="Extract all relevant fields", schema={ "type": "object", "properties": { "name": {"type": "string"}, "date": {"type": "string"}, "amount": {"type": "number"}, } }, advanced_options=AdvancedOptionsExtraction.from_dict({}), parser=Parser.from_dict({}), ) processor = client.create_processor("Invoice Extractor", config) print(f"Created: {processor.id}") ``` Parameter| Type| Description ---|---|--- `name`| `str`| Name for the new processor `config`| `ConfigBase`| Processor configuration object **Returns:** `ProcessorMeta` object **Raises:** `RequestFailed` on error ### Running Processors #### `run_processor(processor_id, file_name, file_url, config) -> ProcessorRun` Execute a processor on a document. ```python run = client.run_processor( processor_id="proc_xxxxxxxxxxxxxxxxx", file_name="invoice.pdf", file_url="https://bucket.s3.amazonaws.com/invoice.pdf", config=None, # Use processor defaults ) print(f"Run ID: {run.id}, Status: {run.status.value}") ``` Parameter| Type| Description ---|---|--- `processor_id`| `str`| Processor to run `file_name`| `str`| Name for the file `file_url`| `str`| Public URL to the document `config`| `ConfigExtraction \| None`| Optional config override (extraction only) **Returns:** `ProcessorRun` object with initial status **Raises:** `RequestFailed` on error #### `run_status(run_id: str) -> ProcessorRun` Get the current status and results of a processor run. ```python run = client.run_status("run_xxxxxxxxxxxxxxxxx") if run.status == RunStatus.PROCESSED: print(f"Result: {run.output.to_dict()}") elif run.status == RunStatus.FAILED: print("Processing failed") else: print(f"Still processing: {run.status.value}") ``` Parameter| Type| Description ---|---|--- `run_id`| `str`| Unique identifier of the run **Returns:** `ProcessorRun` object with current status and results **Raises:** `RequestFailed` on error ## Data Structures ### ProcessorMeta Metadata about a processor. Field| Type| Description ---|---|--- `id`| `str`| Unique processor identifier `name`| `str`| Processor name `type`| `ProcessorType`| Type (EXTRACT, CLASSIFY, SPLITTER) `created_at`| `datetime \| None`| Creation timestamp `updated_at`| `datetime \| None`| Last update timestamp ### ProcessorVersion A specific version of a processor with full configuration. Field| Type| Description ---|---|--- `id`| `str`| Version identifier `version`| `str`| Version name (draft, v1, etc.) `description`| `str`| Version description `processor`| `ProcessorMeta`| Processor metadata `config`| `ConfigClassification \| ConfigExtraction \| ConfigSplitter`| Processor configuration `created_at`| `datetime`| Creation timestamp `updated_at`| `datetime`| Last update timestamp ### ProcessorRun Represents a single execution of a processor. Field| Type| Description ---|---|--- `id`| `str`| Run identifier `processor`| `ProcessorMeta`| Processor that was executed `output`| `ResultClassification \| ResultExtraction \| ResultSplitter \| None`| Processing results `status`| `RunStatus`| Current run status `files`| `list[StoredFile]`| Associated files `usage`| `int`| Total credits consumed ### StoredFile A file stored in Extend AI. Field| Type| Description ---|---|--- `id`| `str`| Unique file identifier `type`| `str`| MIME type / file type `name`| `str`| File name ### Classification A classification category definition. Field| Type| Description ---|---|--- `id`| `str`| Classification identifier `type`| `str`| Classification type/category name `description`| `str`| Description of this classification ## Result Structures ### ResultExtraction Output from an extraction processor. Field| Type| Description ---|---|--- `value`| `dict`| Dictionary of extracted field values **Example:** ```python if run.status == RunStatus.PROCESSED: extracted = run.output.value print(f"Name: {extracted.get('name')}") print(f"Amount: {extracted.get('amount')}") ``` ### ResultClassification Output from a classification processor. Field| Type| Description ---|---|--- `type`| `str`| Assigned classification type `confidence`| `float`| Confidence score (0.0 to 1.0) `insights`| `list[Insight]`| Extracted insights **Example:** ```python if run.status == RunStatus.PROCESSED: print(f"Type: {run.output.type}") print(f"Confidence: {run.output.confidence:.2%}") for insight in run.output.insights: print(f" {insight.type}: {insight.content}") ``` ### ResultSplitter Output from a splitter processor. Field| Type| Description ---|---|--- `splits`| `list[Split]`| List of identified splits **Example:** ```python if run.status == RunStatus.PROCESSED: for split in run.output.splits: print(f"Section: {split.type}") print(f" Pages: {split.startPage} - {split.endPage}") print(f" Observation: {split.observation}") ``` ### Split A document split/section identified by a splitter. Field| Type| Description ---|---|--- `id`| `str`| Split identifier `type`| `str`| Split type/category `observation`| `str`| Observations about this split `identifier`| `str`| Unique identifier `startPage`| `int`| Starting page number `endPage`| `int`| Ending page number `classificationId`| `str`| Associated classification ID `fileId`| `str`| File this split belongs to `name`| `str`| Split name ### Insight An insight extracted during classification. Field| Type| Description ---|---|--- `type`| `str`| Insight type/category `content`| `str`| Insight text content ## Configuration Structures ### ConfigExtraction Configuration for extraction processors. Field| Type| Description ---|---|--- `base_processor`| `BaseProcessor`| Performance or light variant `extraction_rule`| `str`| Custom extraction instructions `schema`| `dict`| JSON Schema for extracted data `advanced_options`| `AdvancedOptionsExtraction`| Advanced settings `parser`| `Parser`| Document parser settings ### ConfigClassification Configuration for classification processors. Field| Type| Description ---|---|--- `classifications`| `list[Classification]`| Possible classification categories `base_processor`| `BaseProcessor`| Performance or light variant `classification_rule`| `str`| Custom classification rules `advanced_options`| `AdvancedOptionsClassification`| Advanced settings `parser`| `Parser`| Document parser settings ### ConfigSplitter Configuration for splitter processors. Field| Type| Description ---|---|--- `split_classifications`| `list[Classification]`| Classification categories for splits `base_processor`| `BaseProcessor`| Performance or light variant `split_rules`| `str`| Custom splitting rules `advanced_options`| `AdvancedOptionsSplitter`| Advanced settings `parser`| `Parser`| Document parser settings ## Constants (Enums) ### ProcessorType Types of processors available. Value| Description ---|--- `EXTRACT`| Extracts structured data based on a schema `CLASSIFY`| Classifies documents into categories `SPLITTER`| Splits documents into sections ### RunStatus Status values for processor runs. Value| Description ---|--- `PENDING`| Run is queued and waiting to start `PROCESSING`| Run is currently being processed `PROCESSED`| Run completed successfully `FAILED`| Run encountered an error `CANCELLED`| Run was cancelled before completion ### VersionName Standard version names for processors. Value| Description ---|--- `LATEST`| Latest published version `DRAFT`| Draft/working version ### BaseProcessor Base processor variants (performance vs speed trade-off). Value| Description ---|--- `CLASSIFICATION_PERFORMANCE`| High accuracy classification `CLASSIFICATION_LIGHT`| Fast classification `EXTRACTION_PERFORMANCE`| High accuracy extraction `EXTRACTION_LIGHT`| Fast extraction `SPLITTING_PERFORMANCE`| High accuracy splitting `SPLITTING_LIGHT`| Fast splitting ## Error Handling ### RequestFailed Exception raised when an Extend AI API request fails (extends `RuntimeError`). Attribute| Type| Description ---|---|--- `status_code`| `int`| HTTP status code `message`| `str`| Error message from Extend AI **Example:** ```python try: run = client.run_processor(...) except RequestFailed as e: print(f"API Error {e.status_code}: {e.message}") ``` ## Polling Pattern Since document processing is asynchronous, use this pattern to wait for results: ```python import time from canvas_sdk.clients.extend_ai.constants import RunStatus def wait_for_completion(client, run_id: str, timeout_seconds: int = 120) -> ProcessorRun: """ Wait for a processor run to complete. Args: client: Extend AI client instance run_id: The run ID to monitor timeout_seconds: Maximum time to wait Returns: ProcessorRun with final status Raises: TimeoutError: If processing exceeds timeout """ start_time = time.time() poll_interval = 2 # seconds while True: run = client.run_status(run_id) # Check if done if run.status not in (RunStatus.PENDING, RunStatus.PROCESSING): return run # Check timeout if time.time() - start_time > timeout_seconds: raise TimeoutError(f"Processing timed out after {timeout_seconds}s") time.sleep(poll_interval) # Usage run = client.run_processor(processor_id, file_name, file_url, None) final_run = wait_for_completion(client, run.id) if final_run.status == RunStatus.PROCESSED: print(final_run.output.to_dict()) ``` ## Additional Resources - [Extend AI Documentation](https://docs.extend.ai/) - [Example Plugin](/sdk/example-extend_ai_pdf/) \- Documentation for the example plugin - [Source Code](https://github.com/canvas-medical/canvas-plugins/tree/main/example-plugins/extend_ai_pdf) \- View the source on GitHub --- # LLMs Source: https://docs.canvasmedical.com/sdk/clients-llms/ The Canvas SDK LLMs client provides a unified interface for interacting with multiple Large Language Model (LLM) providers including OpenAI (GPT Models), Anthropic (Claude), and Google (Gemini). It supports text conversations, file attachments (images, PDFs, text), and structured JSON output. ## Requirements Depending on which LLM provider you use: - **OpenAI** : API key from https://platform.openai.com/api-keys - **Anthropic** : API key from https://console.anthropic.com/settings/keys - **Google** : API key from https://aistudio.google.com/apikey ## Imports The LLMs client is included in the Canvas SDK. Import the necessary components: ```python from canvas_sdk.clients.llms import ( LlmOpenai, LlmAnthropic, LlmGoogle, LlmResponse, LlmTokens, LlmTurn, ) from canvas_sdk.clients.llms.structures.settings import ( LlmSettingsGpt4, LlmSettingsAnthropic, LlmSettingsGemini, ) from canvas_sdk.clients.llms.constants import FileType from canvas_sdk.clients.llms.structures import LlmFileUrl, FileContent, BaseModelLlmJson ``` ## Initialize the Clients ### OpenAI (GPT Models) ```python from canvas_sdk.clients.llms import LlmOpenai from canvas_sdk.clients.llms.structures.settings import LlmSettingsGpt4 client = LlmOpenai(LlmSettingsGpt4( api_key="your_openai_api_key", model="gpt-4o", temperature=0.7, )) ``` ### Anthropic (Claude) ```python from canvas_sdk.clients.llms import LlmAnthropic from canvas_sdk.clients.llms.structures.settings import LlmSettingsAnthropic client = LlmAnthropic(LlmSettingsAnthropic( api_key="your_anthropic_api_key", model="claude-sonnet-4-5-20250929", temperature=0.7, max_tokens=8192, )) ``` ### Google (Gemini) ```python from canvas_sdk.clients.llms import LlmGoogle from canvas_sdk.clients.llms.structures.settings import LlmSettingsGemini client = LlmGoogle(LlmSettingsGemini( api_key="your_google_api_key", model="models/gemini-2.0-flash", temperature=0.7, )) ``` ## Simple Text Conversation ```python from http import HTTPStatus from canvas_sdk.clients.llms import LlmOpenai from canvas_sdk.clients.llms.structures.settings import LlmSettingsGpt4 # Initialize client client = LlmOpenai(LlmSettingsGpt4( api_key="your_api_key", model="gpt-4o", temperature=0.7, )) # Set up the conversation client.set_system_prompt(["You are a helpful assistant."]) client.set_user_prompt(["What is the capital of France?"]) # Make the request response = client.request() if response.code == HTTPStatus.OK: print(f"Response: {response.response}") print(f"Tokens used - Prompt: {response.tokens.prompt}, Generated: {response.tokens.generated}") else: print(f"Error: {response.response}") ``` ## Multi-turn Conversation ```python # Initialize client client = LlmOpenai(LlmSettingsGpt4( api_key="your_api_key", model="gpt-4o", temperature=0.7, )) # Build a multi-turn conversation client.set_system_prompt(["You are a helpful math tutor."]) client.set_user_prompt(["What is 2 + 2?"]) client.set_model_prompt(["2 + 2 equals 4."]) client.set_user_prompt(["And what is that multiplied by 3?"]) # Get the response response = client.request() print(response.response) # "4 multiplied by 3 equals 12." ``` ## Using Retry Logic ```python # Attempt multiple requests until success or max attempts responses = client.attempt_requests(attempts=3) # Check the last response last_response = responses[-1] if last_response.code == HTTPStatus.OK: print(f"Success: {last_response.response}") else: print(f"Failed after {len(responses)} attempts") ``` ## Analyze an Image ```python from canvas_sdk.clients.llms import LlmOpenai from canvas_sdk.clients.llms.structures.settings import LlmSettingsGpt4 from canvas_sdk.clients.llms.constants import FileType from canvas_sdk.clients.llms.structures import LlmFileUrl client = LlmOpenai(LlmSettingsGpt4( api_key="your_api_key", model="gpt-4o", temperature=0.5, )) # Set up prompts client.set_system_prompt(["You are an image analysis assistant."]) client.set_user_prompt(["Describe what you see in this image."]) # Add an image file client.add_url_file(LlmFileUrl( url="https://example.com/image.jpg", type=FileType.IMAGE )) # Get the analysis response = client.request() print(response.response) ``` ## Analyze a PDF Document ```python from canvas_sdk.clients.llms import LlmAnthropic from canvas_sdk.clients.llms.structures.settings import LlmSettingsAnthropic from canvas_sdk.clients.llms.constants import FileType from canvas_sdk.clients.llms.structures import LlmFileUrl client = LlmAnthropic(LlmSettingsAnthropic( api_key="your_api_key", model="claude-sonnet-4-5-20250929", temperature=0.5, max_tokens=4096, )) # Set up prompts client.set_system_prompt(["You are a document analysis assistant."]) client.set_user_prompt(["Summarize the key points in this document."]) # Add a PDF file client.add_url_file(LlmFileUrl( url="https://example.com/document.pdf", type=FileType.PDF )) # Get the summary response = client.request() print(response.response) ``` ## Upload File Content Directly Instead of providing a URL, you can upload file content directly using `FileContent`. This is useful when you have the file data in memory (e.g., from a form upload). ```python import base64 from canvas_sdk.clients.llms import LlmOpenai from canvas_sdk.clients.llms.structures.settings import LlmSettingsGpt4 from canvas_sdk.clients.llms.structures import FileContent client = LlmOpenai(LlmSettingsGpt4( api_key="your_api_key", model="gpt-4o", temperature=0.5, )) # Read file content from disk or form upload with open("document.pdf", "rb") as f: file_bytes = f.read() # Create FileContent with base64-encoded data file_content = FileContent( mime_type="application/pdf", content=base64.b64encode(file_bytes), size=len(file_bytes), ) # Add to the client's file_content list client.file_content.append(file_content) # Set up prompts client.set_system_prompt(["Analyze the provided document."]) client.set_user_prompt(["What are the main topics covered in this document?"]) # Get the analysis response = client.request() print(response.response) ``` **Supported MIME types for direct file content:** MIME Type Pattern| Description ---|--- `image/*`| Images (PNG, JPEG, GIF, etc.) `application/pdf`| PDF documents `text/*`| Text files (Anthropic only) ## Structured JSON Output ```python from pydantic import Field from canvas_sdk.clients.llms import LlmOpenai from canvas_sdk.clients.llms.structures.settings import LlmSettingsGpt4 from canvas_sdk.clients.llms.structures import BaseModelLlmJson # Define your response schema class PersonInfo(BaseModelLlmJson): name: str = Field(description="The person's full name") age: int = Field(description="The person's age in years") occupation: str = Field(description="The person's job or profession") # Initialize client client = LlmOpenai(LlmSettingsGpt4( api_key="your_api_key", model="gpt-4o", temperature=0.3, )) # Set the schema for structured output client.set_schema(PersonInfo) # Set up prompts client.set_system_prompt(["Extract person information from the text."]) client.set_user_prompt(["John Smith is a 35-year-old software engineer."]) # Get structured response response = client.request() # Response will be valid JSON matching the PersonInfo schema print(response.response) # {"name": "John Smith", "age": 35, "occupation": "software engineer"} ``` ## Nested Structured Output ```python from pydantic import Field from canvas_sdk.clients.llms.structures import BaseModelLlmJson # Define nested schemas (all must extend BaseModelLlmJson) class Address(BaseModelLlmJson): street: str = Field(description="Street address") city: str = Field(description="City name") country: str = Field(description="Country name") class Person(BaseModelLlmJson): name: str = Field(description="Full name") address: Address = Field(description="Home address") # Use with client client.set_schema(Person) client.set_system_prompt(["Extract person and address information."]) client.set_user_prompt(["Jane Doe lives at 123 Main St, New York, USA."]) response = client.request() ``` ## LLM Clients All LLM clients inherit from `LlmApi` and share the same interface. ### Available Clients Client| Provider| Settings Class| API Endpoint ---|---|---|--- `LlmOpenai`| OpenAI| `LlmSettingsGpt4`| `https://us.api.openai.com` `LlmAnthropic`| Anthropic| `LlmSettingsAnthropic`| `https://api.anthropic.com` `LlmGoogle`| Google| `LlmSettingsGemini`| `https://generativelanguage.googleapis.com` ### Constructor ```python LlmOpenai(settings: LlmSettingsGpt4) LlmAnthropic(settings: LlmSettingsAnthropic) LlmGoogle(settings: LlmSettingsGemini) ``` ### Attributes Attribute| Type| Description ---|---|--- `settings`| `LlmSettings`| Configuration settings for the LLM API `prompts`| `list[LlmTurn]`| List of conversation turns `file_urls`| `list[LlmFileUrl]`| Files to attach via URL (use `add_url_file()`) `file_content`| `list[FileContent]`| Files to attach via direct content `schema`| `type[BaseModelLlmJson]`| Schema for structured JSON output ### Methods #### `set_system_prompt(text: list[str]) -> None` Set or replace the system prompt. The system prompt is always placed at the beginning of the conversation. **Parameters:** Parameter| Type| Description ---|---|--- `text`| `list[str]`| List of text strings for the prompt #### `set_user_prompt(text: list[str]) -> None` Add a user message to the conversation. **Parameters:** Parameter| Type| Description ---|---|--- `text`| `list[str]`| List of text strings for the prompt #### `set_model_prompt(text: list[str]) -> None` Add a model/assistant response to the conversation history. **Parameters:** Parameter| Type| Description ---|---|--- `text`| `list[str]`| List of text strings for the response #### `add_prompt(prompt: LlmTurn) -> None` Add a conversation turn using an `LlmTurn` object. **Parameters:** Parameter| Type| Description ---|---|--- `prompt`| `LlmTurn`| The conversation turn to add #### `add_url_file(url_file: LlmFileUrl) -> None` Add a file attachment to the next user message. **Parameters:** Parameter| Type| Description ---|---|--- `url_file`| `LlmFileUrl`| File URL and type information #### `set_schema(schema: type[BaseModelLlmJson] | None) -> None` Set a schema for structured JSON output. Pass `None` to disable structured output. **Parameters:** Parameter| Type| Description ---|---|--- `schema`| `type[BaseModelLlmJson] | None`| Pydantic model for JSON schema #### `reset_prompts() -> None` Clear all stored prompts from the conversation. #### `request() -> LlmResponse` Make a single request to the LLM API. **Returns:** `LlmResponse` containing status code, response text, and token usage. #### `attempt_requests(attempts: int) -> list[LlmResponse]` Attempt multiple requests until success or max attempts reached. **Parameters:** Parameter| Type| Description ---|---|--- `attempts`| `int`| Maximum number of request attempts **Returns:** List of all `LlmResponse` objects from each attempt. ## Settings Classes ### LlmSettings (Base) Base configuration class for LLM APIs. Field| Type| Description ---|---|--- `api_key`| `str`| API authentication key `model`| `str`| Model name or identifier ### LlmSettingsGpt4 Settings for OpenAI API. Field| Type| Description ---|---|--- `api_key`| `str`| OpenAI API key `model`| `str`| Model name (e.g., `gpt-4o`, `gpt-4-turbo`) `temperature`| `float`| Randomness control (0.0-2.0) **Example:** ```python LlmSettingsGpt4( api_key="sk-...", model="gpt-4o", temperature=0.7, ) ``` ### LlmSettingsAnthropic Settings for Anthropic Claude API. Field| Type| Description ---|---|--- `api_key`| `str`| Anthropic API key `model`| `str`| Model name (e.g., `claude-sonnet-4-5-20250929`) `temperature`| `float`| Randomness control (0.0-1.0) `max_tokens`| `float`| Maximum tokens to generate **Example:** ```python LlmSettingsAnthropic( api_key="sk-ant-...", model="claude-sonnet-4-5-20250929", temperature=0.7, max_tokens=8192, ) ``` ### LlmSettingsGemini Settings for Google Gemini API. Field| Type| Description ---|---|--- `api_key`| `str`| Google API key `model`| `str`| Model name (e.g., `models/gemini-2.0-flash`) `temperature`| `float`| Randomness control (0.0-2.0) **Example:** ```python LlmSettingsGemini( api_key="AIza...", model="models/gemini-2.0-flash", temperature=0.7, ) ``` ## Data Structures ### LlmResponse Response from an LLM API call. Field| Type| Description ---|---|--- `code`| `HTTPStatus`| HTTP status code of the response `response`| `str`| Text content returned by the LLM `tokens`| `LlmTokens`| Token usage information **Methods:** Method| Returns| Description ---|---|--- `to_dict()`| `dict`| Convert response to dictionary ### LlmTokens Token usage information for LLM API calls. Field| Type| Description ---|---|--- `prompt`| `int`| Number of tokens in the prompt `generated`| `int`| Number of tokens in the generated response **Methods:** Method| Returns| Description ---|---|--- `add(counts)`| `None`| Add token counts from another instance `to_dict()`| `dict`| Convert to dictionary ### LlmTurn A single conversation turn in an LLM interaction. Field| Type| Description ---|---|--- `role`| `str`| Role of the speaker (`system`, `user`, `model`) `text`| `list[str]`| List of text strings for this turn **Methods:** Method| Returns| Description ---|---|--- `to_dict()`| `dict`| Convert turn to dictionary `load_from_dict(dict_list)`| `list[LlmTurn]`| Create turns from list of dicts ### LlmFileUrl Container for file URL and type information. Field| Type| Description ---|---|--- `url`| `str`| URL where the file can be accessed `type`| `FileType`| Type of file (IMAGE, PDF, TEXT) ### FileContent Container for file content, used for direct file uploads to LLM providers. Add instances to `client.file_content` list. Field| Type| Description ---|---|--- `mime_type`| `str`| MIME type of the content (e.g., `image/png`) `content`| `bytes`| Base64-encoded file content `size`| `int`| Size of the original file in bytes **Example:** ```python import base64 from canvas_sdk.clients.llms.structures import FileContent # From file bytes with open("image.png", "rb") as f: file_bytes = f.read() file_content = FileContent( mime_type="image/png", content=base64.b64encode(file_bytes), size=len(file_bytes), ) # Add to client client.file_contents.append(file_content) ``` ### BaseModelLlmJson Base class for structured JSON output schemas. Extends Pydantic's `BaseModel` with: - `additionalProperties: false` in JSON schema - Automatic camelCase field name conversion **Usage:** ```python from pydantic import Field from canvas_sdk.clients.llms.structures import BaseModelLlmJson class MySchema(BaseModelLlmJson): field_name: str = Field(description="Description for the LLM") another_field: int = Field(description="Another description") ``` ## Constants (Enums) ### FileType Supported file types for LLM file attachments. Value| Description ---|--- `IMAGE`| Image files (PNG, JPEG, GIF) `PDF`| PDF documents `TEXT`| Plain text files ### Role Constants Available on all LLM client classes: Constant| Value| Description ---|---|--- `ROLE_SYSTEM`| `"system"`| System/instruction role `ROLE_USER`| `"user"`| User message role `ROLE_MODEL`| `"model"`| Model/assistant response role ## Complete Plugin Example Here's a complete example of using the LLMs client in a Canvas plugin: ```python import base64 from http import HTTPStatus from pydantic import Field from canvas_sdk.clients.llms import LlmOpenai from canvas_sdk.clients.llms.constants import FileType from canvas_sdk.clients.llms.structures import BaseModelLlmJson, FileContent, LlmFileUrl from canvas_sdk.clients.llms.structures.settings import LlmSettingsGpt4 from canvas_sdk.effects import Effect from canvas_sdk.effects.simple_api import JSONResponse, PlainTextResponse, Response from canvas_sdk.handlers.simple_api import Credentials, SimpleAPI, api from canvas_sdk.handlers.simple_api.api import FileFormPart, StringFormPart class AnimalCount(BaseModelLlmJson): """Structured response for animal counting.""" dogs: int = Field(description="Number of dogs in the image") cats: int = Field(description="Number of cats in the image") total: int = Field(description="Total number of animals") class LlmHandler(SimpleAPI): """Simple API handler for LLM operations.""" def authenticate(self, credentials: Credentials) -> bool: return True def _llm_client(self) -> LlmOpenai: """Create LLM client from plugin secrets.""" return LlmOpenai(LlmSettingsGpt4( api_key=self.secrets["LlmKey"], model="gpt-4o", temperature=0.5, )) @api.post("/chat") def chat(self) -> list[Response | Effect]: """Handle a chat conversation.""" client = self._llm_client() # Process conversation turns from request for turn in self.request.json(): if turn.get("role") == "system": client.set_system_prompt([turn.get("prompt", "")]) elif turn.get("role") == "user": client.set_user_prompt([turn.get("prompt", "")]) else: client.set_model_prompt([turn.get("prompt", "")]) response = client.attempt_requests(attempts=2)[0] return [PlainTextResponse(response.response, status_code=response.code)] @api.post("/analyze_image") def analyze_image(self) -> list[Response | Effect]: """Analyze an image for animal content via URL.""" client = self._llm_client() url = self.request.json().get("url") if not url: return [JSONResponse({"error": "URL required"}, status_code=HTTPStatus.BAD_REQUEST)] # Set up structured output client.set_schema(AnimalCount) client.set_system_prompt(["Count the animals in the provided image."]) client.set_user_prompt(["Identify and count all animals in this image."]) client.add_url_file(LlmFileUrl(url=url, type=FileType.IMAGE)) responses = client.attempt_requests(attempts=2) content = [r.to_dict() for r in responses] return [JSONResponse(content, status_code=HTTPStatus.OK)] @api.post("/file") def file(self) -> list[Response | Effect]: """Analyze uploaded file content using LLM. Accepts multipart form data with 'file' and 'input' fields. """ content = b"" mime_type = "" user_input = "" # Parse form data form_data = self.request.form_data() if "file" in form_data and isinstance(form_data["file"], FileFormPart): content = form_data["file"].content mime_type = form_data["file"].content_type if "input" in form_data and isinstance(form_data["input"], StringFormPart): user_input = form_data["input"].value if not (content and mime_type and user_input): return [PlainTextResponse("Missing file or input", status_code=HTTPStatus.BAD_REQUEST)] client = self._llm_client() # Create FileContent with base64-encoded data file = FileContent( mime_type=mime_type, content=base64.b64encode(content), size=len(content), ) client.file_content.append(file) client.set_system_prompt(["Answer the question about the file clearly and concisely."]) client.set_user_prompt([user_input]) response = client.attempt_requests(attempts=1)[0] return [PlainTextResponse(response.response, status_code=response.code)] ``` ## Error Handling The LLM clients return `LlmResponse` objects with HTTP status codes indicating success or failure. ```python from http import HTTPStatus response = client.request() if response.code == HTTPStatus.OK: print(f"Success: {response.response}") elif response.code == HTTPStatus.TOO_MANY_REQUESTS: print("Rate limited - try again later") elif response.code == HTTPStatus.UNAUTHORIZED: print("Invalid API key") elif response.code == HTTPStatus.BAD_REQUEST: print(f"Bad request: {response.response}") else: print(f"Error {response.code}: {response.response}") ``` When using `attempt_requests`, the method will automatically retry on failure: ```python responses = client.attempt_requests(attempts=3) # Check if any attempt succeeded successful = [r for r in responses if r.code == HTTPStatus.OK] if successful: print(f"Success after {len(responses)} attempt(s)") else: print(f"All {len(responses)} attempts failed") ``` ## Provider-Specific Notes ### OpenAI - Uses the Responses API (`/v1/responses`) - Supports images and PDFs via URL (`add_url_file`) or direct content (`file_content`) - System prompts are sent as `instructions` - Direct file content uses `input_image` for images and `input_file` for PDFs ### Anthropic - Uses the Messages API (`/v1/messages`) - Supports images, PDFs, and text files via URL or direct content - Text files are base64-decoded and sent as plain text - Structured output uses tool calling ### Google Gemini - Uses the Generative Language API - Files via URL are downloaded and converted to base64 automatically - Supports both URL-based and direct file content - Maximum file size limit of 10MB per request (combined) - Structured output uses `responseJsonSchema` ## Additional Resources - [OpenAI API Documentation](https://platform.openai.com/docs/api-reference) - [Anthropic API Documentation](https://docs.anthropic.com/en/api) - [Google Gemini API Documentation](https://ai.google.dev/gemini-api/docs) - [Example Plugin](/sdk/example-llm/) \- Documentation for the example plugin - [Source Code](https://github.com/canvas-medical/canvas-plugins/tree/main/example-plugins/llm) \- View the source on GitHub --- # SendGrid Source: https://docs.canvasmedical.com/sdk/clients-sendgrid/ The Canvas SDK SendGrid client provides a simple interface for sending emails, managing webhooks, and querying email logs using the SendGrid API. ## Requirements - **SendGrid API Key** : Create one at https://app.sendgrid.com/settings/api_keys - **Authenticated Domain** : Configure at https://app.sendgrid.com/settings/sender_auth ## Imports The SendGrid client is included in the Canvas SDK. Import the necessary components: ```python from canvas_sdk.clients.sendgrid.libraries import EmailClient from canvas_sdk.clients.sendgrid.constants import RecipientType from canvas_sdk.clients.sendgrid.structures import ( Address, BodyContent, Email, Recipient, RequestFailed, Settings, ) ``` ## Initialize the Client ```python client = EmailClient(Settings(key="your_sendgrid_api_key")) ``` ## Send a Simple Text Email ```python from canvas_sdk.clients.sendgrid.libraries import EmailClient from canvas_sdk.clients.sendgrid.constants import RecipientType from canvas_sdk.clients.sendgrid.structures import ( Address, BodyContent, Email, Recipient, RequestFailed, Settings ) client = EmailClient(Settings(key="your_api_key")) email = Email( sender=Address(email="sender@example.com", name="Sender Name"), reply_tos=[Address(email="reply@example.com", name="Reply To")], recipients=[ Recipient(address=Address(email="recipient@example.com", name="Recipient"), type=RecipientType.TO) ], subject="Hello from Canvas SDK", bodies=[BodyContent(type="text/plain", value="This is a test email.")], attachments=[], send_at=Email.now(), ) try: client.simple_send(email) print("Email sent successfully!") except RequestFailed as e: print(f"Failed to send email: {e.message} (HTTP {e.status_code})") ``` ## Send an HTML Email with CC ```python email = Email( sender=Address(email="sender@example.com", name="Sender"), reply_tos=[Address(email="reply@example.com", name="Reply To")], recipients=[ Recipient(address=Address(email="to@example.com", name="To"), type=RecipientType.TO), Recipient(address=Address(email="cc@example.com", name="CC"), type=RecipientType.CC), ], subject="HTML Email Example", bodies=[ BodyContent(type="text/plain", value="Plain text fallback"), BodyContent(type="text/html", value="

Hello!

This is HTML content.

"), ], attachments=[], send_at=Email.now(), ) client.simple_send(email) ``` ## Send an Email with Attachment ```python from canvas_sdk.clients.sendgrid.structures import Attachment # Create attachment from URL attachment = Attachment.from_url( url="https://example.com/document.pdf", headers={}, filename="document.pdf" ) email = Email( sender=Address(email="sender@example.com", name="Sender"), reply_tos=[Address(email="reply@example.com", name="Reply To")], recipients=[ Recipient(address=Address(email="to@example.com", name="To"), type=RecipientType.TO) ], subject="Email with Attachment", bodies=[BodyContent(type="text/plain", value="Please find attached document.")], attachments=[attachment], send_at=Email.now(), ) client.simple_send(email) ``` ## Send an Email with Inline Image ```python # Create inline image attachment inline_image = Attachment.from_url_inline( url="https://example.com/logo.png", headers={}, filename="logo.png", content_id="logo123" ) email = Email( sender=Address(email="sender@example.com", name="Sender"), reply_tos=[Address(email="reply@example.com", name="Reply To")], recipients=[ Recipient(address=Address(email="to@example.com", name="To"), type=RecipientType.TO) ], subject="Email with Inline Image", bodies=[ BodyContent(type="text/plain", value="See image in HTML version"), BodyContent(type="text/html", value=''), ], attachments=[inline_image], send_at=Email.now(), ) client.simple_send(email) ``` ## Query Sent Emails ```python from datetime import datetime from canvas_sdk.clients.sendgrid.constants import CriterionOperation from canvas_sdk.clients.sendgrid.structures import CriterionDatetime, LoggedEmailCriteria criteria = LoggedEmailCriteria( message_id="", subject="", to_email="recipient@example.com", reason="", status=[], message_created_at=[ CriterionDatetime( date_time=datetime(2024, 1, 1), operation=CriterionOperation.GREATER_THAN_OR_EQUAL ) ], ) for email in client.logged_emails(criteria, up_to=10): print(f"Subject: {email.subject}, Status: {email.status.value}") ``` ## EmailClient The main class for interacting with the SendGrid API. ### Constructor ```python EmailClient(settings: Settings) ``` Parameter| Type| Description ---|---|--- `settings`| `Settings`| Configuration object containing the API key ### Sending Emails #### `simple_send(email: Email) -> bool` Send an email using a structured `Email` object. This is the recommended method for most use cases. **Returns:** `True` on success **Raises:** `RequestFailed` on error #### `prepared_send(data: dict) -> bool` Send an email using a raw dictionary following SendGrid's API schema. Use this for advanced cases not covered by `simple_send`. **Parameters:** - `data`: Dictionary following [SendGrid's mail send schema](https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send#request-body) **Returns:** `True` on success **Raises:** `RequestFailed` on error ### Email Logs #### `logged_emails(criteria: LoggedEmailCriteria, up_to: int) -> Iterator[SentEmail]` Query sent emails matching the specified criteria. **Parameters:** - `criteria`: Filter criteria for the query - `up_to`: Maximum number of results to return **Returns:** Iterator of `SentEmail` objects #### `logged_email(message_id: str) -> SentEmailDetail` Get detailed information about a specific email including its event history. **Parameters:** - `message_id`: The SendGrid message ID **Returns:** `SentEmailDetail` object with full event history ### Inbound Parse Webhooks Configure webhooks to receive incoming emails. Method| Description ---|--- `parser_setting_add(setting: ParseSetting) -> ParseSetting`| Create a new inbound parse webhook `parser_setting_delete(hostname: str) -> bool`| Delete a webhook by hostname `parser_setting_get(hostname: str) -> ParseSetting`| Get webhook configuration by hostname `parser_setting_list() -> Iterator[ParseSetting]`| List all inbound parse webhooks Requires MX record setup pointing to `mx.sendgrid.net`, for example: host| type| priority| TTL| value ---|---|---|---|--- `canvas`| `MX`| 10| 1 hr| `mx.sendgrid.net` ### Event Webhooks Configure webhooks to receive email delivery status notifications. Method| Description ---|--- `event_webhook_add(event: EventWebhook) -> EventWebhookRecord`| Create a new event webhook `event_webhook_delete(event_webhook_id: str) -> bool`| Delete a webhook by ID `event_webhook_get(event_webhook_id: str) -> EventWebhookRecord`| Get webhook by ID `event_webhook_list() -> Iterator[EventWebhookRecord]`| List all event webhooks `event_webhook_sign(event_webhook_id: str, enabled: bool) -> str`| Enable/disable signature verification, returns public key ## Data Structures ### Settings Configuration for the EmailClient. Field| Type| Description ---|---|--- `key`| `str`| SendGrid API key ### Address Represents an email address with display name. Field| Type| Description ---|---|--- `email`| `str`| Email address `name`| `str`| Display name ### Recipient Represents an email recipient with type. Field| Type| Description ---|---|--- `address`| `Address`| Email address object `type`| `RecipientType`| TO, CC, or BCC ### BodyContent Represents email body content with MIME type. Field| Type| Description ---|---|--- `type`| `str`| MIME type (e.g., `text/plain`, `text/html`) `value`| `str`| Content ### Email Complete email message structure. Field| Type| Description ---|---|--- `sender`| `Address`| Sender email address `reply_tos`| `list[Address]`| Reply-to addresses `recipients`| `list[Recipient]`| List of recipients (TO/CC/BCC) `subject`| `str`| Email subject line `bodies`| `list[BodyContent]`| Email body content(s) `attachments`| `list[Attachment]`| File attachments `send_at`| `int`| Unix timestamp for sending **Class Methods:** Method| Description ---|--- `Email.now() -> int`| Get current timestamp for immediate send `Email.timestamp(dt: datetime) -> int`| Convert datetime to Unix timestamp ### Attachment Represents an email attachment. Field| Type| Description ---|---|--- `content_id`| `str`| ID for inline references `content`| `str`| Base64 encoded content `type`| `str`| MIME type `filename`| `str`| Filename `disposition`| `AttachmentDisposition`| ATTACHMENT or INLINE **Class Methods:** Method| Description ---|--- `Attachment.from_url(url, headers, filename) -> Attachment`| Create attachment from URL `Attachment.from_url_inline(url, headers, filename, content_id) -> Attachment`| Create inline attachment from URL ### LoggedEmailCriteria Search criteria for querying sent emails. Field| Type| Description ---|---|--- `message_id`| `str`| Filter by message ID `subject`| `str`| Filter by subject `to_email`| `str`| Filter by recipient email `reason`| `str`| Filter by reason `status`| `list[StatusEmail]`| Filter by status(es) `message_created_at`| `list[CriterionDatetime]`| Filter by creation date/time ### CriterionDatetime DateTime comparison for email queries. Field| Type| Description ---|---|--- `date_time`| `datetime`| Date/time to compare `operation`| `CriterionOperation`| Comparison operator ### SentEmail Basic information about a sent email (returned by `logged_emails`). Field| Type| Description ---|---|--- `from_email`| `str`| Sender email address `message_id`| `str`| SendGrid message ID `subject`| `str`| Email subject `to_email`| `str`| Recipient email address `reason`| `str`| Delivery failure reason `status`| `StatusEmail`| Delivery status `created_at`| `datetime`| Creation timestamp ### SentEmailDetail Detailed sent email information with event history (returned by `logged_email`). Field| Type| Description ---|---|--- `from_email`| `str`| Sender email address `message_id`| `str`| SendGrid message ID `subject`| `str`| Email subject `to_email`| `str`| Recipient email address `status`| `StatusEmail`| Current delivery status `events`| `list[EmailEvent]`| List of lifecycle events ### EmailEvent Represents an event in an email's lifecycle. Field| Type| Description ---|---|--- `event`| `EventEmail`| Event type `email`| `str`| Recipient email address `message_id`| `str`| SendGrid message ID `event_id`| `str`| Unique event ID `on_datetime`| `datetime`| Event timestamp `reason`| `str`| Reason (for bounce, dropped events) `response`| `str`| Server response (for delivered events) `url`| `str`| Clicked/opened URL (for click, open) `attempt`| `int`| Delivery attempt number (for deferred) ### ParseSetting Configuration for inbound email parsing. Field| Type| Description ---|---|--- `url`| `str`| Webhook URL to receive parsed emails `hostname`| `str`| Domain to receive emails (requires MX record) `spam_check`| `bool`| Enable spam filtering `send_raw`| `bool`| Send raw MIME message instead of parsed ### EventWebhook Configuration for outbound email event notifications. Field| Type| Description ---|---|--- `enabled`| `bool`| Whether webhook is active `url`| `str`| Webhook URL `friendly_name`| `str`| Display name `delivered`| `bool`| Track delivered events `bounce`| `bool`| Track bounce events `dropped`| `bool`| Track dropped events `spam_report`| `bool`| Track spam report events `processed`| `bool`| Track processed events `open`| `bool`| Track open events `click`| `bool`| Track click events `unsubscribe`| `bool`| Track unsubscribe events `group_resubscribe`| `bool`| Track group resubscribe events `group_unsubscribe`| `bool`| Track group unsubscribe events ### EventWebhookRecord Stored event webhook with metadata (extends EventWebhook). Field| Type| Description ---|---|--- _(all fields from EventWebhook)_| | `id`| `str`| Webhook ID `public_key`| `str`| Public key for signature verification `created_date`| `datetime`| Creation timestamp `updated_date`| `datetime`| Last update timestamp ### ParsedEmail Represents an inbound email received via the Inbound Parse webhook. Field| Type| Description ---|---|--- `headers`| `list[ParsedHeader]`| Email headers `charsets`| `dict[str, str]`| Character set mappings `envelope`| `ParsedEnvelope`| SMTP envelope information `email_from`| `str`| Sender address `email_to`| `str`| Recipient address `subject`| `str`| Email subject `text`| `str`| Plain text body `html`| `str`| HTML body `attachments`| `int`| Number of attachments `attachment_info`| `dict[str, ParsedAttachment]`| Attachment metadata `content_ids`| `dict[str, str]`| Content ID mappings `spf`| `str`| SPF verification result `dkim`| `str`| DKIM verification result `spam_report`| `list[str]`| Spam analysis report `spam_score`| `float`| Spam score ## Constants (Enums) ### RecipientType Value| Description ---|--- `TO`| Primary recipient `CC`| Carbon copy `BCC`| Blind carbon copy ### AttachmentDisposition Value| Description ---|--- `ATTACHMENT`| Standard file attachment `INLINE`| Embedded in email body ### StatusEmail Email delivery status values. Value| Description ---|--- `PROCESSED`| Email processed by SendGrid `DELIVERED`| Successfully delivered `NOT_DELIVERED`| Delivery failed `DEFERRED`| Temporarily delayed `DROPPED`| Dropped by SendGrid `BOUNCED`| Bounced back `BLOCKED`| Blocked by recipient ### EventEmail Email event types for webhooks. Value| Description ---|--- `BOUNCE`| Email bounced `CLICK`| Link clicked `DEFERRED`| Delivery deferred `DELIVERED`| Email delivered `DROPPED`| Email dropped `CANCEL_DROP`| Drop cancelled `OPEN`| Email opened `PROCESSED`| Email processed `RECEIVED`| Inbound email received `SPAM_REPORT`| Reported as spam `GROUP_UNSUBSCRIBE`| Unsubscribed from group `GROUP_RESUBSCRIBE`| Resubscribed to group `UNSUBSCRIBE`| Unsubscribed ### CriterionOperation Comparison operators for email log queries. Value| Symbol| Description ---|---|--- `GREATER_THAN`| `>`| Greater than `GREATER_THAN_OR_EQUAL`| `>=`| Greater than or equal `LOWER_THAN`| `<`| Less than `LOWER_THAN_OR_EQUAL`| `<=`| Less than or equal `EQUAL`| `=`| Equal to ## Error Handling ### RequestFailed Exception raised when a SendGrid API request fails (extends `RuntimeError`). Attribute| Type| Description ---|---|--- `status_code`| `int`| HTTP status code `message`| `str`| Error message from SendGrid **Example:** ```python try: client.simple_send(email) except RequestFailed as e: print(f"Error {e.status_code}: {e.message}") ``` ## Webhook Setup Examples ### Inbound Parse Webhook (Receive Incoming Emails) ```python from canvas_sdk.clients.sendgrid.structures import ParseSetting # Note: Requires MX record for the hostname pointing to mx.sendgrid.net setting = ParseSetting( url="https://your-app.com/api/incoming-email", hostname="mail.yourdomain.com", spam_check=True, send_raw=False, ) try: created = client.parser_setting_add(setting) print(f"Inbound webhook created for {created.hostname}") except RequestFailed as e: print(f"Failed: {e.message}") ``` ### Event Webhook (Track Outbound Email Status) ```python from canvas_sdk.clients.sendgrid.structures import EventWebhook webhook = EventWebhook( url="https://your-app.com/api/email-events", enabled=True, friendly_name="My Email Tracker", delivered=True, bounce=True, dropped=True, spam_report=True, processed=True, open=True, click=True, unsubscribe=False, group_resubscribe=False, group_unsubscribe=False, ) try: created = client.event_webhook_add(webhook) print(f"Event webhook created with ID: {created.id}") except RequestFailed as e: print(f"Failed: {e.message}") ``` ## Additional Resources - [SendGrid API Documentation](https://www.twilio.com/docs/sendgrid/api-reference) - [Inbound Parse Webhook Setup](https://www.twilio.com/docs/sendgrid/for-developers/parsing-email/inbound-email) - [Event Webhook Documentation](https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event) - [Example Plugin](/sdk/example-sendgrid_email/) \- Documentation for the example plugin - [Source Code](https://github.com/canvas-medical/canvas-plugins/tree/main/example-plugins/sendgrid_email) \- View the source on GitHub --- # Twilio Source: https://docs.canvasmedical.com/sdk/clients-twilio/ The Canvas SDK Twilio client provides a simple interface for sending SMS and MMS messages, managing phone numbers, and handling webhooks using the Twilio API. ## Requirements - **Twilio Account SID** : Found in your [Twilio Console](https://console.twilio.com/) - **Twilio API Key and Secret** : Create at [API Keys](https://console.twilio.com/us1/account/keys-credentials/api-keys) - **Twilio Phone Number** : Purchase at [Phone Numbers](https://console.twilio.com/us1/develop/phone-numbers/manage/incoming) ## Imports The Twilio client is included in the Canvas SDK. Import the necessary components: ```python from canvas_sdk.clients.twilio.libraries import SmsClient from canvas_sdk.clients.twilio.structures import Settings, SmsMms, RequestFailed ``` ## Initialize the Client ```python settings = Settings( account_sid="your_account_sid", key="your_api_key", secret="your_api_secret", ) client = SmsClient(settings) ``` ## Send a Simple SMS ```python from canvas_sdk.clients.twilio.libraries import SmsClient from canvas_sdk.clients.twilio.structures import Settings, SmsMms, RequestFailed # Initialize the client settings = Settings( account_sid="ACxxxxxxxxxxxxxxxxx", key="SKxxxxxxxxxxxxxxxxx", secret="your_api_secret", ) client = SmsClient(settings) # First, get your phone number SID phones = list(client.account_phone_numbers()) phone = phones[0] # Use the first phone number print(f"Using phone: {phone.phone_number} (SID: {phone.sid})") # Create and send the SMS sms = SmsMms( number_from=phone.phone_number, number_from_sid=phone.sid, number_to="+1234567890", message="Hello from Canvas SDK!", media_url="", status_callback_url="", ) try: message = client.send_sms_mms(sms) print(f"Message sent! SID: {message.sid}, Status: {message.status.value}") except RequestFailed as e: print(f"Failed to send: {e.message} (HTTP {e.status_code})") ``` ## Send an MMS with Image ```python # Create an MMS with an image attachment mms = SmsMms( number_from=phone.phone_number, number_from_sid=phone.sid, number_to="+1234567890", message="Check out this image!", media_url="https://example.com/image.jpg", status_callback_url="", ) try: message = client.send_sms_mms(mms) print(f"MMS sent! SID: {message.sid}") except RequestFailed as e: print(f"Failed to send: {e.message}") ``` ## Send SMS with Status Callback ```python # Send SMS with a callback URL to track delivery status sms = SmsMms( number_from=phone.phone_number, number_from_sid=phone.sid, number_to="+1234567890", message="Message with tracking", media_url="", status_callback_url="https://your-app.com/api/sms-status", ) message = client.send_sms_mms(sms) print(f"Message queued with callback. SID: {message.sid}") ``` ## Retrieve Message History ```python from canvas_sdk.clients.twilio.constants import DateOperation # Get all messages (no filters) for message in client.retrieve_all_sms("", "", "", DateOperation.ON_EXACTLY): print(f"{message.date_sent}: {message.number_from} -> {message.number_to}: {message.body}") # Get messages sent to a specific number for message in client.retrieve_all_sms("+1234567890", "", "", DateOperation.ON_EXACTLY): print(f"To {message.number_to}: {message.body}") # Get messages from a specific date onwards for message in client.retrieve_all_sms("", "", "2024-01-01", DateOperation.ON_AND_AFTER): print(f"{message.date_sent}: {message.body}") ``` ## Handle Inbound Messages (Webhook) When Twilio receives an SMS to your number, it can call your webhook. Parse the callback data: ```python from canvas_sdk.clients.twilio.structures import StatusInbound, TwiMlMessage def handle_inbound_sms(raw_body: str) -> str: """Process incoming SMS and return TwiML response.""" # Parse the incoming message inbound = StatusInbound.callback_inbound(raw_body) print(f"Received from {inbound.number_from}: {inbound.body}") # Create a reply using TwiML if "hello" in inbound.body.lower(): reply = TwiMlMessage.instance("Hello! Nice to hear from you!") else: reply = TwiMlMessage.instance("Thanks for your message!") return reply.to_xml() ``` ## Reply with MMS (TwiML) ```python from canvas_sdk.clients.twilio.structures import TwiMlMessage # Create a TwiML response with text and image reply = TwiMlMessage.instance_with_media( message_text="Here's a picture for you!", media_url="https://example.com/image.jpg" ) xml_response = reply.to_xml() # Returns TwiML like: # # Here's a picture for you!https://example.com/image.jpg ``` ## SmsClient The main class for interacting with the Twilio SMS/MMS API. ### Constructor ```python SmsClient(settings: Settings) ``` Parameter| Type| Description ---|---|--- `settings`| `Settings`| Configuration object with Twilio credentials ### Phone Number Management #### `account_phone_numbers() -> Iterator[Phone]` Retrieve all phone numbers associated with the Twilio account. ```python for phone in client.account_phone_numbers(): print(f"{phone.friendly_name}: {phone.phone_number}") print(f" SMS: {phone.capabilities.sms}, MMS: {phone.capabilities.mms}") ``` **Returns:** Iterator of `Phone` objects **Raises:** `RequestFailed` on error #### `account_phone_number(phone_sid: str) -> Phone` Retrieve details for a specific phone number by its SID. ```python phone = client.account_phone_number("PNxxxxxxxxxxxxxxxxx") print(f"Phone: {phone.phone_number}, Status: {phone.status}") ``` Parameter| Type| Description ---|---|--- `phone_sid`| `str`| The Twilio SID of the phone **Returns:** `Phone` object **Raises:** `RequestFailed` on error #### `set_inbound_webhook(phone_sid: str, webhook_url: str, method: HttpMethod) -> bool` Configure the webhook URL for receiving inbound messages on a phone number. ```python from canvas_sdk.clients.twilio.constants import HttpMethod success = client.set_inbound_webhook( phone_sid="PNxxxxxxxxxxxxxxxxx", webhook_url="https://your-app.com/api/inbound-sms", method=HttpMethod.POST ) ``` Parameter| Type| Description ---|---|--- `phone_sid`| `str`| The Twilio SID of the phone `webhook_url`| `str`| URL to receive inbound messages `method`| `HttpMethod`| HTTP method (GET or POST) **Returns:** `True` on success **Raises:** `RequestFailed` on error ### Sending Messages #### `send_sms_mms(sms_mms: SmsMms) -> Message` Send an SMS or MMS message. The method automatically validates phone capabilities. ```python sms = SmsMms( number_from="+15551234567", number_from_sid="PNxxxxxxxxxxxxxxxxx", number_to="+15559876543", message="Hello!", media_url="", # Empty for SMS, URL for MMS status_callback_url="https://your-app.com/status", ) message = client.send_sms_mms(sms) print(f"Sent! SID: {message.sid}, Status: {message.status.value}") ``` Parameter| Type| Description ---|---|--- `sms_mms`| `SmsMms`| Message details to send **Returns:** `Message` object with sent message details **Raises:** `RequestFailed` if the phone lacks required capabilities or API fails ### Retrieving Messages #### `retrieve_sms(message_id: str) -> Message` Get details for a specific message by its SID. ```python message = client.retrieve_sms("SMxxxxxxxxxxxxxxxxx") print(f"Status: {message.status.value}") print(f"Body: {message.body}") print(f"Sent: {message.date_sent}") ``` Parameter| Type| Description ---|---|--- `message_id`| `str`| The Twilio message SID **Returns:** `Message` object **Raises:** `RequestFailed` on error #### `retrieve_all_sms(number_to, number_from, date_sent, date_operation) -> Iterator[Message]` Retrieve messages with optional filtering. ```python from canvas_sdk.clients.twilio.constants import DateOperation # All messages for msg in client.retrieve_all_sms("", "", "", DateOperation.ON_EXACTLY): print(msg.body) # Messages to a specific number for msg in client.retrieve_all_sms("+15551234567", "", "", DateOperation.ON_EXACTLY): print(msg.body) # Messages from a specific date for msg in client.retrieve_all_sms("", "", "2024-06-01", DateOperation.ON_AND_AFTER): print(f"{msg.date_sent}: {msg.body}") ``` Parameter| Type| Description ---|---|--- `number_to`| `str`| Filter by recipient (empty = no filter) `number_from`| `str`| Filter by sender (empty = no filter) `date_sent`| `str`| Date to filter by (YYYY-MM-DD format) `date_operation`| `DateOperation`| How to compare the date **Returns:** Iterator of `Message` objects **Raises:** `RequestFailed` on error #### `delete_sms(message_id: str) -> bool` Delete a message from Twilio. ```python deleted = client.delete_sms("SMxxxxxxxxxxxxxxxxx") print(f"Deleted: {deleted}") ``` Parameter| Type| Description ---|---|--- `message_id`| `str`| The Twilio message SID **Returns:** `True` on success **Raises:** `RequestFailed` on error ### Media Handling #### `retrieve_media_list(message_id: str) -> Iterator[Media]` Get all media attachments for a message. ```python for media in client.retrieve_media_list("SMxxxxxxxxxxxxxxxxx"): print(f"Media SID: {media.sid}") print(f"Content Type: {media.content_type}") ``` Parameter| Type| Description ---|---|--- `message_id`| `str`| The Twilio message SID **Returns:** Iterator of `Media` objects **Raises:** `RequestFailed` on error #### `retrieve_raw_media(message_id: str, media_sid: str) -> bytes` Download the raw binary content of a media attachment. ```python for media in client.retrieve_media_list(message_sid): content = client.retrieve_raw_media(message_sid, media.sid) # Save to file with open(f"media_{media.sid}.jpg", "wb") as f: f.write(content) ``` Parameter| Type| Description ---|---|--- `message_id`| `str`| The Twilio message SID `media_sid`| `str`| The Twilio media SID **Returns:** Raw binary content (`bytes`) **Raises:** `RequestFailed` on error ## Data Structures ### Settings Configuration for the SmsClient. Field| Type| Description ---|---|--- `account_sid`| `str`| Twilio Account SID `key`| `str`| Twilio API Key SID `secret`| `str`| Twilio API Key Secret ### SmsMms Represents an SMS or MMS message to send. Field| Type| Description ---|---|--- `number_from`| `str`| Sender phone number (E.164 format) `number_from_sid`| `str`| Twilio SID of the sender phone number `number_to`| `str`| Recipient phone number (E.164 format) `message`| `str`| Text content of the message `media_url`| `str`| URL of media to attach (empty for SMS) `status_callback_url`| `str`| URL to receive delivery status updates ### Message Represents a Twilio message with full metadata. Field| Type| Description ---|---|--- `sid`| `str`| Unique message identifier `body`| `str`| Message text content `date_created`| `datetime`| When the message was created `date_sent`| `datetime \| None`| When the message was sent `date_updated`| `datetime`| When the message was last updated `direction`| `MessageDirection`| Message direction `number_from`| `str`| Sender phone number `number_to`| `str`| Recipient phone number `price`| `str \| None`| Cost of the message `price_unit`| `str`| Currency of the price `error_code`| `int \| None`| Error code if failed `error_message`| `str \| None`| Error description if failed `uri`| `str`| API URI for this resource `count_media`| `int \| None`| Number of media attachments `count_segments`| `int`| Number of SMS segments `status`| `MessageStatus`| Current message status `sub_resource_uris`| `dict[str, str] \| None`| URIs to related resources ### Phone Represents a Twilio phone number with configuration. Field| Type| Description ---|---|--- `account_sid`| `str`| Twilio Account SID `capabilities`| `Capabilities`| Phone capabilities (SMS, MMS, etc.) `date_created`| `datetime`| When added to account `date_updated`| `datetime`| Last configuration update `friendly_name`| `str`| User-defined name `phone_number`| `str`| Phone number in E.164 format `sid`| `str`| Unique phone number identifier `sms_fallback_method`| `HttpMethod`| HTTP method for fallback URL `sms_fallback_url`| `str`| Fallback URL if primary fails `sms_method`| `HttpMethod`| HTTP method for SMS webhook `sms_url`| `str`| Webhook URL for inbound SMS `status_callback_method`| `HttpMethod`| HTTP method for status callbacks `status_callback`| `str`| URL for status updates `status`| `str`| Current phone number status ### Capabilities Phone number communication capabilities. Field| Type| Description ---|---|--- `fax`| `bool`| Supports fax `mms`| `bool`| Supports MMS (multimedia) `sms`| `bool`| Supports SMS (text) `voice`| `bool`| Supports voice calls ### Media Represents media attached to a message. Field| Type| Description ---|---|--- `sid`| `str`| Unique media identifier `content_type`| `str`| MIME type (e.g., `image/jpeg`) `date_created`| `datetime`| When the media was created `date_updated`| `datetime`| When the media was last updated `parent_sid`| `str`| Message SID this media belongs to `uri`| `str`| API URI for this resource ### StatusInbound Parsed data from an inbound message webhook callback. Field| Type| Description ---|---|--- `account_sid`| `str`| Twilio Account SID `message_sid`| `str`| Message SID `messaging_service_sid`| `str`| Messaging Service SID `sms_message_sid`| `str`| SMS Message SID `sms_sid`| `str`| SMS SID `sms_status`| `MessageStatus`| Message status `to_country`| `str`| Recipient country `to_zip`| `str`| Recipient ZIP code `to_state`| `str`| Recipient state `to_city`| `str`| Recipient city `from_country`| `str`| Sender country `from_zip`| `str`| Sender ZIP code `from_state`| `str`| Sender state `from_city`| `str`| Sender city `number_to`| `str`| Recipient phone number `number_from`| `str`| Sender phone number `body`| `str`| Message text `count_media`| `int`| Number of media attachments `count_segments`| `int`| Number of SMS segments `media_content_type`| `list[str]`| MIME types of attached media `media_url`| `list[str]`| URLs of attached media **Class Methods:** Method| Description ---|--- `StatusInbound.callback_inbound(raw_body)`| Parse URL-encoded webhook body ### StatusOutboundApi Parsed data from an outbound message status callback. Field| Type| Description ---|---|--- `account_sid`| `str`| Twilio Account SID `message_sid`| `str`| Message SID `sms_sid`| `str`| SMS SID `sms_status`| `MessageStatus`| SMS status `message_status`| `MessageStatus`| Message status `number_to`| `str`| Recipient phone number `number_from`| `str`| Sender phone number **Class Methods:** Method| Description ---|--- `StatusOutboundApi.callback_outbound_api(raw_body)`| Parse URL-encoded webhook body ### TwiMlMessage Generates TwiML XML for responding to inbound messages. Field| Type| Description ---|---|--- `number_to`| `str`| Recipient (optional in response) `number_from`| `str`| Sender (optional in response) `status_callback_url`| `str`| Status callback URL `message_text`| `str`| Message text content `media_url`| `str`| Media URL to attach `method`| `HttpMethod\|None`| HTTP method for callbacks **Class Methods:** Method| Description ---|--- `TwiMlMessage.instance(message_text) -> TwiMlMessage`| Create text-only TwiML message `TwiMlMessage.instance_with_media(message_text, media_url) -> TwiMlMessage`| Create TwiML with media **Instance Methods:** Method| Description ---|--- `to_xml() -> str`| Generate TwiML XML string **Example:** ```python # Simple text reply reply = TwiMlMessage.instance("Thanks for your message!") xml = reply.to_xml() # Reply with media reply = TwiMlMessage.instance_with_media("Check this out!", "https://example.com/image.jpg") xml = reply.to_xml() ``` ## Constants (Enums) ### MessageStatus Message lifecycle status values. Value| Description ---|--- `ACCEPTED`| Message accepted by Twilio `SCHEDULED`| Message scheduled for future delivery `CANCELED`| Scheduled message was canceled `QUEUED`| Message queued for sending `SENDING`| Message is being sent `SENT`| Message sent to carrier `FAILED`| Message failed to send `DELIVERED`| Message delivered to recipient `UNDELIVERED`| Message could not be delivered `PARTIALLY_DELIVERED`| Some recipients received the message `RECEIVING`| Inbound message being received `RECEIVED`| Inbound message received `READ`| Message was read (WhatsApp only) ### MessageDirection Message direction types. Value| Description ---|--- `INBOUND`| Message received from external number `OUTBOUND_API`| Message sent via API `OUTBOUND_CALL`| Message sent during a call `OUTBOUND_REPLY`| Message sent as webhook reply ### DateOperation Date filtering operations for message queries. Value| Description ---|--- `ON_EXACTLY`| Messages on exactly this date `ON_AND_BEFORE`| Messages on or before this date `ON_AND_AFTER`| Messages on or after this date ### HttpMethod HTTP methods for webhook configuration. Value| Description ---|--- `GET`| HTTP GET method `POST`| HTTP POST method ## Error Handling ### RequestFailed Exception raised when a Twilio API request fails (extends `RuntimeError`). Attribute| Type| Description ---|---|--- `status_code`| `int`| HTTP status code `message`| `str`| Error message from Twilio **Example:** ```python try: message = client.send_sms_mms(sms) except RequestFailed as e: if e.status_code == 0: # Client-side validation error (e.g., phone lacks MMS capability) print(f"Validation error: {e.message}") else: # Twilio API error print(f"API error {e.status_code}: {e.message}") ``` ## Complete Webhook Example Here's a complete example of handling inbound SMS and sending replies: ```python from canvas_sdk.clients.twilio.structures import StatusInbound, TwiMlMessage def handle_webhook(raw_body: str) -> str: """ Handle incoming SMS webhook from Twilio. Args: raw_body: URL-encoded form data from Twilio POST request Returns: TwiML XML response string """ # Parse the inbound message inbound = StatusInbound.callback_inbound(raw_body) # Log the message print(f"From: {inbound.number_from}") print(f"To: {inbound.number_to}") print(f"Body: {inbound.body}") print(f"Media count: {inbound.count_media}") # Check for media attachments if inbound.count_media > 0: for i, url in enumerate(inbound.media_url): print(f"Media {i}: {inbound.media_content_type[i]} - {url}") # Generate appropriate response body_lower = inbound.body.lower() if "help" in body_lower: reply = TwiMlMessage.instance("Commands: HELP, STATUS, HELLO") elif "hello" in body_lower: reply = TwiMlMessage.instance_with_media( "Hello! Here's a welcome image!", "https://example.com/welcome.jpg" ) elif "status" in body_lower: reply = TwiMlMessage.instance("System is operational.") else: reply = TwiMlMessage.instance("Unknown command. Text HELP for options.") return reply.to_xml() ``` ## Additional Resources - [Twilio SMS API Documentation](https://www.twilio.com/docs/sms) - [Twilio Webhooks Guide](https://www.twilio.com/docs/messaging/guides/webhook-request) - [TwiML Reference](https://www.twilio.com/docs/messaging/twiml) - [Example Plugin](/sdk/example-twilio_sms_mms/) \- Documentation for the example plugin - [Source Code](https://github.com/canvas-medical/canvas-plugins/tree/main/example-plugins/twilio_sms_mms) \- View the source on GitHub --- # Clients Source: https://docs.canvasmedical.com/sdk/clients/ The clients module provides pre-built integrations with popular third-party services, letting your plugins send emails, SMS messages, interact with AI models, process documents, and manage cloud storage. Each client handles authentication, request formatting, and response parsing so you can focus on your plugin's logic. All clients follow a consistent pattern: configure credentials via [plugin secrets](/sdk/secrets/), instantiate a client, and call methods. Error handling is standardized with a `RequestFailed` exception across most clients. > **Warning:** When using third-party clients with your own API keys, you are responsible for all privacy, security, and regulatory compliance associated with those services. For certain providers such as OpenAI and Anthropic, you may contact Canvas to inquire about access through our compliant accounts. [ AWS S3Upload, download, and manage files in Amazon S3. ](/sdk/clients-aws-s3/)[ Canvas FHIRInteract with the Canvas FHIR API for resources like Coverages and DocumentReferences. ](/sdk/clients-canvas-fhir/)[ Extend AIIntelligent document processing with extraction, classification, and splitting. ](/sdk/clients-extend-ai/)[ LLMsUnified interface for OpenAI, Anthropic, and Google AI models. ](/sdk/clients-llms/)[ SendGridSend emails, manage webhooks, and track delivery with SendGrid. ](/sdk/clients-sendgrid/)[ TwilioSend SMS/MMS messages and manage phone numbers with Twilio. ](/sdk/clients-twilio/) --- # Command Metadata Create form Source: https://docs.canvasmedical.com/sdk/command-metadata-create-form-effect/ The `CommandMetadataCreateFormEffect` allows developers to dynamically display additional fields with a command in a note. The values entered in these fields are stored as [command metadata](/sdk/data-command/#commandmetadata) against the target `command_uuid`. Build a `CommandMetadataCreateFormEffect` with the form fields to show, then return its `apply()` from a `COMMAND__FORM__GET_ADDITIONAL_FIELDS` handler. ## Methods ### apply() → Effect Displays the form fields with the command. - `command_uuid` and `form_fields` are required. - `options` may only be set on fields whose `type` is `InputType.SELECT`; providing `options` on a `TEXT` or `DATE` field raises a validation error. - Every `key` must be unique across `form_fields`. Duplicates raise a validation error per duplicated key. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `command_uuid`| `str`| The id of the [command](/sdk/data-command/) these fields should be rendered on.| Yes `form_fields`| `list[``FormField``]`| List of fields.| Yes ## FormField Attribute| Type| Description ---|---|--- `key`| `str`| Unique identifier of the field; the command metadata key. `label`| `str`| The label displayed on the field. `type`| `InputType`| The type of the input: `TEXT`, `SELECT`, or `DATE`. Defaults to `TEXT`. `required`| `bool`| Whether the input is required. Defaults to `False`. `editable`| `bool`| Whether the input can be edited. Defaults to `True`. `options`| `list[str]`| Possible options when the input type is `SELECT`. Only allowed on `SELECT` fields. `value`| `str`| Default value used only when no command metadata row exists for this key. If the user has previously saved a value (including a cleared or empty value), the stored row wins and this field is ignored. ## Example The following handler declares two extra fields on every plan command when the platform requests additional fields for it: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.command_metadata import ( CommandMetadataCreateFormEffect, FormField, InputType, ) from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class PlanCommandAdditionalFields(BaseHandler): RESPONDS_TO = EventType.Name(EventType.COMMAND__FORM__GET_ADDITIONAL_FIELDS) def compute(self) -> list[Effect]: # Only respond for plan commands. if self.event.context.get("schema_key") != "plan": return [] form = CommandMetadataCreateFormEffect( command_uuid=self.event.target.id, form_fields=[ FormField( key="priority", label="Priority", type=InputType.SELECT, options=["low", "medium", "high"], ), FormField( key="follow_up_date", label="Follow-up date", type=InputType.DATE, editable=True, ), ], ) return [form.apply()] ``` Once the user fills out these fields, their values are persisted as command metadata and can be read back through the SDK [command metadata](/sdk/data-command/#commandmetadata) table. ## Rendering on the printed note The same `COMMAND__FORM__GET_ADDITIONAL_FIELDS` event fires when a command is rendered for printing (single-command print URL or full note printout). The platform uses the response to label and order the fields shown beneath each command in the printed output. Two things differ from the chart-form render path: - **Values come from stored command metadata, not from`FormField.value`.** The platform pairs each field declared in your effect with the matching `CommandMetadata` row by `key`. Whatever value is on `FormField` is ignored during print rendering. You do not need to populate `value` for print. - **Fields with no stored value or a blank value are skipped.** Only fields the user actually filled in will appear in the printout. - **Fields you do not declare are hidden.** A `CommandMetadata` row whose `key` is not in your response will not print, even if it exists in the database. This matches the chart UI: removing or renaming a key in your effect makes the prior data invisible. ### Branching on `purpose` The event context carries a `purpose` key indicating which call site triggered the request: Value| When ---|--- `"form"`| Chart UI is rendering the command's edit form (default). `"print"`| Single-command printout or note printout is being generated. Read it from the handler context to vary your response — for example, to omit internal fields from print, or shorten labels for a denser layout. ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.command_metadata import ( CommandMetadataCreateFormEffect, FormField, InputType, ) from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class PlanCommandAdditionalFields(BaseHandler): RESPONDS_TO = EventType.Name(EventType.COMMAND__FORM__GET_ADDITIONAL_FIELDS) def compute(self) -> list[Effect]: if self.event.context.get("schema_key") != "plan": return [] is_print = self.event.context.get("purpose") == "print" fields = [ FormField( key="priority", label="Priority", type=InputType.SELECT, options=["low", "medium", "high"], ), FormField( key="follow_up_date", label="Follow-up date", type=InputType.DATE, ), ] if not is_print: # Internal-only: visible on the form, hidden from printouts. fields.append( FormField( key="reviewer_notes", label="Reviewer notes", type=InputType.TEXT, ) ) return [ CommandMetadataCreateFormEffect( command_uuid=self.event.target.id, form_fields=fields, ).apply() ] ``` ### Tips for the print path - **Use clear, human-readable`label` values.** Whatever you put on `FormField.label` is what the printed output shows. The platform does not derive a label from the `key`. - **Use the same`key` you used when persisting metadata.** The print path joins on `key`; mismatches mean nothing renders for that field. - **`type`, `options`, and `required` are ignored at print time.** Only `key` and `label` shape the printout. - **Need to retire a field?** Removing it from the print response hides it for all future prints — including for committed notes. If you need the historical value to keep showing on signed records, keep the field declared (or declare it only when `purpose == "print"`). --- # Custom Command Source: https://docs.canvasmedical.com/sdk/commands-custom-command/ ## Introduction The `CustomCommand` class allows plugins to create custom commands with HTML-rendered content that can be inserted into patient charts. Custom commands are designed for displaying read-only content and do not support user input or interactive forms. **Important** : Custom commands must be configured in the plugin's [`CANVAS_MANIFEST.json`](/sdk/canvas_manifest/#commands) file under the `commands` array before they can be used. ## Parameters Name| Type| Required| Description ---|---|---|--- `note_uuid`| _string_| `true`| The externally exposable id of the note in which to insert the command. `command_uuid`| _string_| `true`| The externally exposable id of the command which is being referenced. `schema_key`| _string_| `true`| Identifier for data binding. Must match the `schema_key` in your manifest configuration and must be unique across every plugin installed on the instance. `content`| _string_| `true`| HTML content for display in the chart. `print_content`| _string_| `false`| HTML content for print version (recommended for optimal print output). ## Manifest Configuration Custom commands must be declared in your `CANVAS_MANIFEST.json`: ```json { "components": { "commands": [ { "name": "RiskAssessment", "label": "Risk Assessment", "schema_key": "riskAssessment", "section": "assessment" } ] } } ``` ### Manifest Fields - **name** : Unique name for the command - **label** : User-friendly label displayed in Canvas UI - **schema_key** : Identifier for the command. Must be unique across every plugin installed on the Canvas instance — if another installed plugin already declares the same `schema_key`, installation will be rejected. CustomCommand instances must use this value. - **section** : Chart section where command appears: `subjective`, `objective`, `assessment`, `plan`, `procedures`, `history`, or `internal` **Note** : `schema_key` values must be unique across **all plugins installed on the instance** , not just within a single plugin. If you install a plugin whose manifest declares a `schema_key` already owned by another installed plugin, the installation fails with a clear validation error instead of silently overwriting the existing command. Choose a distinctive `schema_key` — for example, prefixing it with your plugin's name — to avoid collisions. ## Basic Usage ### Step 1: Create HTML Templates Create a template file for your command content (e.g., `templates/risk_assessment.html`): ```html
Risk Assessment
Cardiovascular Risk: High

Hypertension, family history of heart disease

Falls Risk: Moderate

Age over 65, history of dizziness

``` Create a simpler print version (e.g., `templates/risk_assessment_print.html`): ```html

Risk Assessment

Cardiovascular Risk: High - Hypertension, family history
Falls Risk: Moderate - Age over 65, history of dizziness
``` ### Step 2: Use Templates in Your Command ```python from canvas_sdk.commands.commands.custom_command import CustomCommand from canvas_sdk.templates import render_to_string import uuid command = CustomCommand( schema_key="riskAssessment", content=render_to_string("templates/risk_assessment.html"), print_content=render_to_string("templates/risk_assessment_print.html") ) command.command_uuid = str(uuid.uuid4()) command.note_uuid = "rk786p" effect = command.originate() ``` ### Extended CustomCommand Class (For Reusability) Create a subclass with a predefined `schema_key`: ```python from canvas_sdk.commands.commands.custom_command import CustomCommand from canvas_sdk.templates import render_to_string import uuid class RiskAssessmentCommand(CustomCommand): """Custom command for risk assessment.""" class Meta: schema_key = "riskAssessment" # Usage command = RiskAssessmentCommand( content=render_to_string("templates/risk_assessment.html"), print_content=render_to_string("templates/risk_assessment_print.html") ) command.command_uuid = str(uuid.uuid4()) command.note_uuid = "rk786p" effect = command.originate() ``` ## Methods ### originate() Returns an Effect that originates a new command in the note body. **Example:** ```python from canvas_sdk.commands.commands.custom_command import CustomCommand from canvas_sdk.templates import render_to_string import uuid command = CustomCommand( schema_key="riskAssessment", content=render_to_string("templates/risk_assessment.html"), print_content=render_to_string("templates/risk_assessment_print.html") ) command.command_uuid = str(uuid.uuid4()) command.note_uuid = "rk786p" effect = command.originate() ``` ## Content vs Print Content Custom commands support two versions of content: ### Display Content (content) - Rendered in the Canvas UI when viewing the chart - Can include rich styling and complex layouts ### Print Content (print_content) - Rendered when printing the chart or generating PDFs - Should be simpler and more compact **Best Practice** : Always provide both versions for the best user experience. ## Collapsible Content The `content` frame stops responding to clicks once the command is committed or the note is locked, so a toggle inside `content` can't be opened in a finished note. To show a long, read-only record without it taking up the note, attach it with [`set_custom_html`](/sdk/commands/#set_custom_html) inside a `
` element. Custom HTML renders under the command's title in its own frame. That frame keeps responding to clicks after the command is committed or the note is locked, and it grows when the provider opens the `
` element. - Set the custom HTML while the command is still staged: return `set_custom_html` in the same list as `originate()`, before any `commit()`. - `content` is still required. Keep it to a short summary; the custom HTML appears above it. - Custom HTML isn't printed, so put the full record in `print_content`. ```python from canvas_sdk.commands.commands.custom_command import CustomCommand from canvas_sdk.templates import render_to_string import uuid full_record = render_to_string("templates/intake_record.html") command = CustomCommand( schema_key="intakeRecord", content="

Intake submitted. 3 answers flagged for review.

", print_content=full_record ) command.command_uuid = str(uuid.uuid4()) command.note_uuid = "rk786p" effects = [ command.originate(), command.set_custom_html( f"
Show full intake{full_record}
" ), ] ``` ## Limitations - Custom commands are read-only and cannot capture user input - Interactive elements (forms, buttons, input fields) are not supported, and the `content` frame stops responding to clicks once the command is committed or the note is locked. To let providers expand long content, see Collapsible Content. - Commands must be configured in the manifest before use - The `schema_key` must be unique across every plugin installed on the Canvas instance. Installing a plugin whose `schema_key` is already owned by another installed plugin will fail with a validation error. --- # Commands Source: https://docs.canvasmedical.com/sdk/commands/ The commands module lets you create and update commands within a specific note in Canvas. Commands are the building blocks of many end-user workflows in Canvas, including nearly all clinical workflows for documentation, like HPIs and questionnaires, as well as orders like prescriptions, labs, and referrals. Each Command class can be instantiated in your plugin and used to build a new command instance within a specific note or update an existing instance. The commands are then displayed in real time within the end user's workflow. Common objectives that can be met by using Command classes include dynamic note templates, clinical decision support, order set composition, care gap closure, and care coordination automation. Commands are written from an event handler by default. To let something outside Canvas write them — a patient-facing form, a device, an internal tool — expose them over HTTP with [`CommandAPI`](/sdk/handlers-simple-api-commands/), which reads a request body onto any command on this page, validates it, and emits the effects. The [Writing Commands Over HTTP](/guides/writing-commands-over-http/) guide walks through building one. > **Info:** New to command fields? Fields that are autocompletes, dropdowns, or enums in the Canvas UI take a raw code, id, or enum value in the SDK — you have to look the value up first. See [Populating Command Fields](/guides/populating-command-fields/) for where each value comes from. Note content such as HPI, Review of Systems, Physical Exam, Assessment, and Plan is populated programmatically through this commands module. A common pattern for external integrations is to expose a [SimpleAPI](/sdk/handlers-simple-api-http/) endpoint that uses the [Create Note](/sdk/effect-notes/#create-note) effect to create the note and then originates commands into it. See the [charting API examples](/sdk/example-charting_api_examples/) plugin for a worked example. To write commands from an outside system over HTTP, use [`CommandAPI`](/sdk/handlers-simple-api-commands/), which turns a command into an endpoint and validates each request against the command. To go beyond the built-in commands, by adding fields, showing your own content, or adding commands of your own, see [Customizing Commands Beyond the Built-ins](/guides/customizing-commands/). "Commands" is an umbrella term for all the structured data within a patient's note. Questionnaire is one specific command type. When a questionnaire is built, its use case in charting can be set to Physical Exam, Structured Assessment, Review of Systems, or Questionnaire; these all share the same underlying database structure but appear in the note as their own distinct command. The `.originate()` method works on all command types. You can use the SDK to auto-populate questionnaire commands (and other commands) in response to an event rather than building many separate per-situation automations. Listen for an appropriate [event](/sdk/events/) — such as a note state change, another command being committed, or metadata being added — and originate the commands you need. You can also insert commands when an [action button](/sdk/handlers-action-buttons/) is clicked if a manual trigger is preferred. ## Common Attributes ### Parameters All commands share the following init kwarg parameters: Name| Type| Required| Description ---|---|---|--- `note_uuid`| _string_| `true` if creating a new command| The id of the [Note](/sdk/data-note/#note) in which to insert the command. `command_uuid`| _string_| `true` if updating an existing command| The id of the [Command](/sdk/data-command/#command). On `originate` you can pass your own value to set it the first time; when updating, it references an existing command. All parameters can be set upon initialization, and also updated on the class instance. Field values are read leniently, so a value does not have to arrive already in the field's own type: a number can be given as `"3"`, a date as `"2026-08-04"`, and an enum as its value (`"mild"`) rather than the member. This matters most when the values come from somewhere that only has strings, such as a JSON request body. The **Required to** column (depending on the command's terminal action) in each command's parameter table below indicates whether a field must be set in order for the server to accept that terminal action. It does **not** mean the field is required to instantiate the class or to call `.originate()` or `.edit()`. You can create a command with every other field empty and the command appears in the note as fresh command, mirroring how a clinician picks a command from the menu in the UI and leaves it unfilled until they're ready to finalize it. The only field required to **originate** any command is `note_uuid`. Skip `.commit()` and the command persists in the note in its empty/staged state until the user (or a later plugin call) fills it in and commits it: ```python from canvas_sdk.commands import PrescribeCommand # inside a handler's compute(), on an event whose context carries the note p = PrescribeCommand() p.note_uuid = self.event.context["note"]["uuid"] return [p.originate()] ``` The same pattern works with any command class — `PlanCommand(note_uuid=...)`, `AssessCommand(note_uuid=...)`, `MedicalHistoryCommand(note_uuid=...)`, etc. ### Methods **Not every command supports every method.** `originate` is the only one they all have; `edit`, `delete`, `commit`, `enter_in_error`, `review`, `send`, `delegate` and `sign` each depend on the command. The [command type table](/sdk/effects/#commands) lists the actions each command accepts — check it before relying on one. `upsert_metadata` works on any command, and `set_custom_html` works on any staged command, including [custom commands](/sdk/commands-custom-command/#collapsible-content). To call these over HTTP rather than from a handler, see [`CommandAPI`](/sdk/handlers-simple-api-commands/#methods). #### originate Returns an Effect that originates a new command in the note body. **Parameters:** Name| Type| Required| Default| Description ---|---|---|---|--- `commit`| `bool`| No| `False`| When `True`, the command is automatically committed after origination. This is a simpler alternative to returning separate `originate()` and `commit()` effects. **Note:** This only applies to command types that support the COMMIT action. Commands that do not support committing (Reason For Visit, Prescribe, Refill, Adjust Prescription, Refer, and Order commands) will ignore this parameter. See the [command type table](/sdk/effects/#commands) for which commands support COMMIT. `line_number`| `int`| No| `-1`| The line number in the note where the command should be inserted. By default the command will insert at the bottom of the note. **See also:** For efficiently inserting multiple commands at once, see [Batch Originate Commands](/sdk/effect-batch-originate/). **Examples** : ```python from canvas_sdk.commands import PlanCommand def compute(): new_plan = PlanCommand(note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', narrative='new') new_plan.narrative = 'newer' return [new_plan.originate()] ``` To originate and commit in a single effect: ```python from canvas_sdk.commands import AssessCommand def compute(): assess_command = AssessCommand( note_uuid='550e8400-e29b-41d4-a716-446655440000', icd10_code='E11.9' ) return [assess_command.originate(commit=True)] ``` #### edit Returns an Effect that edits an existing command with the values set on the command class instance. **Behavior and Considerations:** - **Partial Edits:** If you update only some fields of the command, any fields not explicitly modified will retain their existing values. - **No Changes:** Calling `edit()` without making any changes will result in a no-op; the command remains unchanged. - **Invalid Values:** If you attempt to set an invalid value, you should receive a validation error. **Example** : ```python from canvas_sdk.commands import PlanCommand def compute(): existing_plan = PlanCommand(command_uuid='2b9d1f0a-4c3e-4b5a-9d8c-7e6f5a4b3c2d', narrative='something new') return [existing_plan.edit()] ``` #### delete Returns an Effect that deletes an existing, non-committed command from the note body. **Example** : ```python from canvas_sdk.commands import PlanCommand def compute(): existing_plan = PlanCommand(command_uuid='2b9d1f0a-4c3e-4b5a-9d8c-7e6f5a4b3c2d') return [existing_plan.delete()] ``` To delete the command that triggered your handler, instantiate the matching command class with `command_uuid=self.target` and return its `delete()` effect — for example, `PlanCommand(command_uuid=self.target).delete()` from a handler responding to `PLAN_COMMAND__POST_ORIGINATE`. If you instead want to mark a committed command as entered in error, use `enter_in_error`. Note that `delete()` only removes non-committed (staged) commands. #### commit Returns an Effect that commits an existing, non-committed command to the note body. To block a command from committing and surface a message to the user — for example, enforcing your own business rules before a command is entered — return a [Command Validation effect](/sdk/effect-command-validation/) from a handler on the command's validation event. **Example** : ```python from canvas_sdk.commands import PlanCommand def compute(): existing_plan = PlanCommand(command_uuid='2b9d1f0a-4c3e-4b5a-9d8c-7e6f5a4b3c2d') return [existing_plan.commit()] ``` #### review Returns an Effect that sets a command in review. **Limited availability** The `review()` method can only be called on Prescribe commands objects. Other command types do not support this operation. **Example** : ```python from canvas_sdk.commands import PrescribeCommand def compute(): existing_prescribe = PrescribeCommand(command_uuid='e32b85d9-ccb7-4e4f-a0e5-8783ed2d9528') return [existing_prescribe.review()] ``` #### send Returns an Effect that sends a signed command. **Limited availability** The `send()` method can only be called on LabOrder, Prescribe, Refill and AdjustPrescription command objects. Other command types do not support this operation. The three prescribing commands share one set of electronic prescribing validations. **Parameters:** Name| Type| Required| Default| Description ---|---|---|---|--- `practice_location_override`| `str \| UUID`| No| `None`| Prescribe only. The `id` of a [PracticeLocation](/sdk/data-practicelocation/#practicelocation) whose address is used as the prescriber address on the outgoing prescription, overriding the prescriber's primary location. See Prescribe for behavior and limitations. **Example** : ```python from canvas_sdk.commands import PrescribeCommand def compute(): existing_prescribe = PrescribeCommand(command_uuid='e32b85d9-ccb7-4e4f-a0e5-8783ed2d9528') return [existing_prescribe.send()] ``` To send the prescription using a specific practice location's address (see Prescribe): ```python from canvas_sdk.commands import PrescribeCommand def compute(): existing_prescribe = PrescribeCommand(command_uuid='e32b85d9-ccb7-4e4f-a0e5-8783ed2d9528') return [existing_prescribe.send(practice_location_override='a1b2c3d4-e5f6-7890-abcd-ef1234567890')] ``` #### enter_in_error Returns an effect that enter-in-errors an existing, committed command in the note body. The command referenced by `command_uuid` must already be committed; returning this effect transitions it to entered-in-error status. **Example** : ```python from canvas_sdk.commands import PlanCommand def compute(): existing_plan = PlanCommand(command_uuid='2b9d1f0a-4c3e-4b5a-9d8c-7e6f5a4b3c2d') return [existing_plan.enter_in_error()] ``` #### delegate Returns an Effect that delegates an existing, staged command by creating a task. **Limited availability** The `delegate()` method can only be called on ImagingOrder and Refer command objects. Other command types do not support this operation. **Example** : ```python from canvas_sdk.commands import ReferCommand def compute(): existing_refer = ReferCommand(command_uuid='e32b85d9-ccb7-4e4f-a0e5-8783ed2d9528') return [existing_refer.delegate()] ``` #### sign Returns an Effect that signs an existing, staged command, transitioning it to a committed state. **Limited availability** The `sign()` method can only be called on ImagingOrder, OrderDme, and Refer command objects. Other command types do not support this operation. **Example** : ```python from canvas_sdk.commands import ImagingOrderCommand def compute(): existing_imaging_order = ImagingOrderCommand(command_uuid='e32b85d9-ccb7-4e4f-a0e5-8783ed2d9528') return [existing_imaging_order.sign()] ``` #### upsert_metadata Returns a [CommandMetadata effect](/sdk/effect-command-metadata/) that creates or updates a metadata key-value pair on a command. If metadata with the given key already exists on the command, its value will be updated. Otherwise, a new metadata record will be created. The `command_uuid` field must be set on the command object before calling `upsert_metadata`. To make this metadata **visible and editable as fields on the command in the note** — rather than only stored behind the scenes — use the [Command Metadata Create Form effect](/sdk/command-metadata-create-form-effect/), which renders additional fields on the command whose values are saved as command metadata. Parameter| Type| Description ---|---|--- `key`| _string_| The metadata key (max 256 characters). `value`| _string_| The metadata value. **Example** : ```python from canvas_sdk.commands import PlanCommand def compute(): existing_plan = PlanCommand(command_uuid='2b9d1f0a-4c3e-4b5a-9d8c-7e6f5a4b3c2d') return [existing_plan.upsert_metadata(key="priority", value="high")] ``` #### set_custom_html Returns an effect that sets or clears custom HTML content on a command. The HTML is stored on the command and rendered alongside it in the note. The `command_uuid` field must be set on the command object before calling `set_custom_html`. The command must be in a staged (not committed) state—calling this method on a committed command will raise a validation error. Parameter| Type| Description ---|---|--- `custom_html`| _string_ or _None_| The HTML content to set on the command, or `None` to clear it. **Example** : ```python from canvas_sdk.commands import PlanCommand def compute(): existing_plan = PlanCommand(command_uuid='2b9d1f0a-4c3e-4b5a-9d8c-7e6f5a4b3c2d') return [existing_plan.set_custom_html("
Important note
")] ``` To clear existing custom HTML from a command: ```python from canvas_sdk.commands import PlanCommand def compute(): existing_plan = PlanCommand(command_uuid='2b9d1f0a-4c3e-4b5a-9d8c-7e6f5a4b3c2d') return [existing_plan.set_custom_html(None)] ``` ## Originating and Committing Together The simplest way to originate and commit a command in a single plugin action is to pass `commit=True` to the `originate()` method: ```python from canvas_sdk.commands import AssessCommand def compute(): assess_command = AssessCommand( note_uuid='550e8400-e29b-41d4-a716-446655440000', icd10_code='E11.9' ) return [assess_command.originate(commit=True)] ``` This handles the origination and commit in a single effect, without needing to manage a `command_uuid` yourself. ### Chaining Methods with a User-set UUID If you need more control over the process — for example, to edit a command between origination and commit — you can chain separate effects by setting the `command_uuid` manually. This chaining is necessary because a handler returns all of its effects at once, so it cannot read back an id that Canvas assigns during origination. ```python from uuid import uuid4 from canvas_sdk.commands import AssessCommand def compute(): note_uuid = '550e8400-e29b-41d4-a716-446655440000' assess_command = AssessCommand( note_uuid=note_uuid, icd10_code='E11.9' ) # To chain command effects, you must know what the command's id # is. To accomplish that, we set the id ourselves rather than # allow the database to assign one. assess_command.command_uuid = str(uuid4()) # Now we can both originate and commit in a single operation return [assess_command.originate(), assess_command.commit()] ``` This pattern ensures that both the originate and commit operations use the same `command_uuid`, allowing them to be chained together reliably in a single plugin execution. Command-specific details for each command class can be found below. ## Command Actions All commands support user-triggered actions through the Canvas UI. These actions appear as buttons or menu items that users can click to perform operations on a command. Commands have two types of actions: - **Generic actions** — available on all commands (listed below). - **Command-specific actions** — vary by command type and are documented in each command's section below. Action| Description ---|--- `print`| Generates a printable version of the command for documentation or external sharing. `audit_history`| Displays the complete audit trail for the command, showing all modifications, state changes, and user interactions over time. `carry_forward`| Populates the command with the last known data for this command type and patient, letting users quickly recreate a similar command from a previous entry. How a printed note orders its commands depends on the note's format. A note in the older format, or any note on an instance outside the [revamped note beta program](/product-updates/note-revamp/), groups its commands by section in SOAP order, and this sorting can't be turned off. Within each section, commands print in a fixed order by command type, not in the order they were entered. A note in the revamped format prints the sections it shows on screen, History, Exam, and Assessment & Plan, in the note's SOAP or APSO order. Within each section, commands print in the order the provider arranged them in the note. Each Assess command prints with the commands [linked to it](/sdk/data-assessment/#commands-linked-to-an-assessment) indented below it. The Internal section isn't printed, and free text prints under a Notes heading after the sections. Faxed notes and the patient's entire-record print use the same layout. To control the print layout yourself, build a plugin that adds an [action button](/sdk/handlers-action-buttons/) with a custom print template. The [patient-visit-summary](https://github.com/medical-software-foundation/canvas/tree/main/extensions/patient-visit-summary) extension does this: staff choose and reorder the sections of a note, preview the result, and print it or save it as a PDF. > **Info:** The send action is the only command action available through the SDK, and only LabOrder, Prescribe, Refill and Adjust Prescription commands support it. ### Customizing Action Availability You can programmatically control which actions appear on a command — and in what order — by responding to that command's `AVAILABLE_ACTIONS` event. Common uses: - **Hide actions** based on user permissions, role, or command state. - **Reorder actions** to prioritize commonly used operations. - **Conditionally show actions** depending on workflow or business logic. **How it works:** 1. When Canvas renders a command, it fires that command's `_COMMAND__AVAILABLE_ACTIONS` event (e.g. `PLAN_COMMAND__AVAILABLE_ACTIONS`). 2. Your handler receives the default action list in `self.context["actions"]` and the acting user in `self.context["user"]`. 3. Return a single `COMMAND_AVAILABLE_ACTIONS_RESULTS` effect whose payload is the action list you want rendered. The returned list **replaces** the default set, so include every action the user should see — returning the original list unchanged is a no-op. **Example** — hide the `print` action for a specific user: ```python import json from canvas_sdk.handlers import BaseHandler from canvas_sdk.effects import Effect, EffectType from canvas_sdk.events import EventType from canvas_sdk.v1.data import Staff class Handler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.PLAN_COMMAND__AVAILABLE_ACTIONS) def compute(self) -> list[Effect]: actions = self.context["actions"] user_id = self.context["user"]["staff"] try: staff = Staff.objects.get(id=user_id) # Hide the print action for this user; everyone else keeps the full set if staff.first_name == "Larry": filtered_actions = [a for a in actions if a["name"] != "print"] else: filtered_actions = actions except Staff.DoesNotExist: filtered_actions = actions return [ Effect( type=EffectType.COMMAND_AVAILABLE_ACTIONS_RESULTS, payload=json.dumps(filtered_actions), ) ] ``` ## Command Validation Beyond the built-in validation each command performs on its own fields, you can add your **own** validation rules to a command and surface error messages to the user directly in the Canvas UI. A handler responds to a command's validation event (for example, `PLAN_COMMAND__POST_VALIDATION`) and returns a [Command Validation effect](/sdk/effect-command-validation/) containing one or more error messages. This is useful for enforcing organization-specific business rules — such as requiring a field, restricting certain combinations, or blocking a command until an external condition is met — before the command can be committed. See the [Command Validation effect](/sdk/effect-command-validation/) documentation for the full API and examples. ## Choosing Between Similar Commands Some commands write to the same part of the chart and differ only in intent. The tables below cover the cases where more than one command could plausibly apply. ### Recording a condition Which command fits depends on whether the condition is active now, whether you are assessing it, and whether it is surgical. Command| Use it when ---|--- Assess with `icd10_code`| The condition is active and you are assessing it, whether it is new or already on the chart. It reuses the patient's charted condition with that code, and records a new one only when the patient has none, so it never adds a duplicate. Use this for new work. Set `show_in_problem_list=False` for a diagnosis that belongs only to this visit. MedicalHistory| You are backfilling a condition the patient had in the past that is not active now. AddCondition| The condition was diagnosed previously and is still active, and you are not assessing it. SurgicalHistory| You are recording a past surgical procedure. Diagnose| Still available for a new active condition you are assessing for the first time, but planned for retirement in a future release. Use Assess with `icd10_code` instead. Each of these writes a [Condition](/sdk/data-condition/#condition) when it records a new one. They differ in what they set on it, which is what decides where it shows up and which commands can act on it later: Command| `clinical_status`| `surgical`| Opens an assessment ---|---|---|--- Assess with `icd10_code`| `active`| `false`| Yes MedicalHistory| `resolved`| `false`| No AddCondition| `active`| `false`| No SurgicalHistory| `resolved`| `true`| No Diagnose| `active`| `false`| Yes Surgical history is coded in SNOMED rather than ICD-10, and the `surgical` flag is what separates it from past medical history: Remove Past Medical History rejects a surgical entry, and AddCondition ignores one when checking whether a condition is already on the list. ### Updating or removing a condition Four more commands act on a condition that is already on the chart. Command| Use it when ---|--- Assess| You are recording an assessment against a condition on the chart, named by `condition_id` or `icd10_code`. Assessing a resolved condition makes it active again. UpdateDiagnosis| The diagnosis was wrong or has been refined, and you want the new code to carry the original's history. Resolve Condition| An active condition is no longer relevant to track. It must be committed, not entered in error, and not already resolved. Remove Past Medical History| A past medical history entry does not belong on the chart at all. It must have no committed assessment against it. Resolve Condition will not take it, because the entry is already resolved. The last two are easy to confuse. Resolve Condition closes a condition that was genuinely present and leaves it on the record as resolved. Remove Past Medical History withdraws an entry that should not have been recorded, taking it off the conditions list entirely. ### Recording a medication Six commands touch a patient's medication list. Three of them produce a prescription that can be sent to a pharmacy with `send()`, and three only change what the chart says. Command| Use it when| Produces a prescription ---|---|--- Prescribe| You are prescribing a medication the patient is not already on.| Yes Refill| You are renewing a medication the patient is already on. The `fdb_code` must match one of that patient's active medications.| Yes AdjustPrescription| You are replacing a prescribed medication with a different one, given as `new_fdb_code`. A different strength or formulation is a different FDB code, so it goes here too.| Yes ChangeMedication| You are changing the `sig` on an active medication without issuing a new prescription.| No StopMedication| The patient is stopping a medication, and you want the reason on the chart.| No MedicationStatement| You are recording a medication the patient reports taking, without prescribing it.| No Canceling a prescription that has already gone to a pharmacy is a Cancel Prescription command in the note. There is no SDK command class for it, so a plugin cannot originate one, though the resulting records are readable through the [CancelPrescription](/sdk/data-cancel-prescription/) data module. ## Commands The sections below document each command class. See Common Attributes for the parameters and methods shared by all commands. ### AddCondition Records a coded (ICD-10) condition on the patient's Conditions list with clinical status active, in the note's History section, without opening an assessment for it. Reach for it when the condition was diagnosed previously and is still active. See Recording a condition for how it compares to Assess and MedicalHistory. **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `icd10_code`| _string_| `true`| ICD-10 code of the condition to record. Search with the [ICD-10 condition endpoint](/sdk/utils/#get-icdcondition--icd-10-conditions). `background`| _string_| `false`| Background information about the condition. `approximate_date_of_onset`| _date_| `false`| The approximate date the condition began. `comments`| _string_| `false`| Additional comments (max length: 1000 characters). **Example** : ```python from canvas_sdk.commands import AddConditionCommand from datetime import date add_condition = AddConditionCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', icd10_code='N183', background='Diagnosed at outside nephrology practice.', approximate_date_of_onset=date(2023, 4, 1), comments='Stable on last three panels.' ) ``` > **Info:** Once the condition this command recorded carries an assessment, the command can no longer be entered in error, because reverting it would take the condition out from under that assessment. This holds for plugins as well as for the note: an `ENTER_IN_ERROR` effect against such a command is rejected with _Command could not be entered in error due to command-specific business logic_. Resolve the condition instead. * * * ### AdjustPrescription Replaces a prescribed medication with a different one, and can send the new prescription to a pharmacy. See Recording a medication for how it compares to the other medication commands. **Command-specific parameters** : Name| Type| Required to review / send| Description ---|---|---|--- `new_fdb_code`| _string_| `true`| The [FDB code](/sdk/utils/#fdb_code) of the new medication. Check the Prescribe command for the other parameters used in the Adjust Prescription command. Adjust Prescription supports `send()` under the same electronic prescribing validations. ```python from canvas_sdk.commands import AdjustPrescriptionCommand, PrescribeCommand from canvas_sdk.commands.constants import ClinicalQuantity AdjustPrescriptionCommand( fdb_code="172480", new_fdb_code="216092", icd10_codes=["R51"], sig="Take one tablet daily after meals", days_supply=30, quantity_to_dispense=30, type_to_dispense=ClinicalQuantity( representative_ndc="12843016128", ncpdp_quantity_qualifier_code="C48542" ), refills=3, substitutions=PrescribeCommand.Substitutions.ALLOWED, pharmacy="pharmacy_ncpdp_id", prescriber_id="a7c2e9d1-3b4f-4a6c-8e0d-5f1a2b3c4d5e", supervising_provider_id="c3d4e5f6-7a8b-4c9d-0e1f-2a3b4c5d6e7f", note_to_pharmacist="Please verify patient's insurance before processing." ) ``` * * * ### Allergy **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `allergy`| _Allergen_| `false`| Represents the allergen. See details in the Allergen type below. Search allergens with the [ontologies allergen search](/sdk/utils/#get-fdballergy--full-text-search). `severity`| _Severity enum_| `false`| The severity of the allergic reaction. Must be one of `AllergyCommand.Severity`. `narrative`| _string_| `false`| A narrative or free-text description of the allergy (max length: 512 characters). `approximate_date`| _datetime_| `false`| The approximate date the allergy was identified. **Enums and Types** : **`Allergen`** Attribute| Type| Description ---|---|--- `concept_id`| _integer_| The identifier for the allergen concept. `concept_type`| _AllergenType enum_| The type of allergen. See `AllergenType` values below. AllergenType| Value| Description ---|---|--- `ALLERGEN_GROUP`| `1`| Represents a group of allergens. `MEDICATION`| `2`| Represents a medication allergen. `INGREDIENT`| `6`| Represents an ingredient allergen. Severity| Value| Description ---|---|--- `MILD`| `"mild"`| Indicates a mild reaction. `MODERATE`| `"moderate"`| Indicates a moderate reaction. `SEVERE`| `"severe"`| Indicates a severe reaction. **Example** : ```python from canvas_sdk.commands.commands.allergy import AllergyCommand, AllergenType, Allergen from datetime import date allergy = AllergyCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", allergy=Allergen(concept_id=12345, concept_type=AllergenType.MEDICATION), severity=AllergyCommand.Severity.SEVERE, narrative="Severe rash and difficulty breathing after penicillin.", approximate_date=date(2023, 6, 15) ) ``` * * * ### Assess Records an assessment of a patient's condition. Name the condition in one of two ways: - `icd10_code` (recommended) assesses a condition by its ICD-10 code, whether or not it's already on the chart, so there's no need to look up a condition id first. If the patient already has a charted condition with that code, the assessment uses it, preferring an active condition over a resolved one. Committing the command makes a resolved condition active again. Codes match regardless of dots and case, so `M54.50` and `M5450` name the same condition. If the patient has no charted condition with that code, committing the command records a new one, on the problem list unless `show_in_problem_list` is `False`. - `condition_id` assesses a specific condition already on the patient's chart. Set one or the other, not both. You can originate an Assess with neither, for example to leave an empty command for the user to complete, but committing it needs a condition. See Recording a condition and Updating or removing a condition for how it compares to the other condition commands. > **Info:** If your plugin relies on [`ASSESS_COMMAND__CONDITION_SELECTED`](/sdk/events/#assess_command__condition_selected) or [`ASSESS_COMMAND__ICD10_CODE_SELECTED`](/sdk/events/#assess_command__icd10_code_selected), set the condition with `edit()`. These events fire when an existing Assess command's condition changes, so originating an Assess with `condition_id` or `icd10_code` already set sends neither. To trigger them, originate the command without a condition, then `edit()` it to set one. An `edit()` with `icd10_code` sends `ASSESS_COMMAND__ICD10_CODE_SELECTED` even when the patient already has that condition charted. Entering a committed Assess in error also enters in error the condition its commit recorded, so withdrawing the assessment doesn't leave a diagnosis on the chart that no one made. The condition stays on the chart while another committed assessment that isn't entered in error still uses it. Conditions the Assess didn't record, such as one already on the chart or one recorded by a Diagnose or AddCondition command, are never changed. You can always enter an Assess in error, even when other assessments use the condition it recorded. Set `show_in_problem_list` to choose whether the condition belongs on the patient's problem list or only to this visit. Committing with `False` records the condition as an encounter diagnosis, such as a sore throat that doesn't need tracking. Committing with `True` moves an encounter diagnosis back onto the problem list. The choice is saved as the condition's category, which the [FHIR Condition](/api/condition/) resource returns as `encounter-diagnosis` or `problem-list-item`. An encounter diagnosis is still one assessed condition, not a copy, so Plan commands and billing in the note can use it. The chart's Conditions list shows encounter diagnoses alongside problem list items. Clinicians can pick one in Update Diagnosis or Resolve Condition, or as a Prescribe indication, and an UpdateDiagnosis command can name it by code. A commit that changes the status sends [`CONDITION_UPDATED`](/sdk/events/#condition_updated). Entering in error an Assess that changed the status resets the condition's status. The condition takes the status set by the most recently committed Assess that changed it and isn't entered in error. If none remains, it goes back to the status it had before any Assess changed it. **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `condition_id`| _string_| `true`, unless `icd10_code` is set| The id of the [Condition](/sdk/data-condition/#condition) being assessed. Must be a condition already recorded on that patient's chart. Can't be combined with `icd10_code`. `icd10_code`| _string_| `true`, unless `condition_id` is set| ICD-10 code of the condition being assessed. Search with the [ICD-10 condition endpoint](/sdk/utils/#get-icdcondition--icd-10-conditions). Can't be combined with `condition_id`. `approximate_date_of_onset`| _date_| `false`| The approximate date the condition began. Committing the command saves it as the condition's onset date, replacing any date already recorded. If you leave it unset, the condition's onset date doesn't change. `show_in_problem_list`| _boolean_| `false`| Whether committing the command puts the condition on the problem list (`True`) or records it as an encounter diagnosis for this visit only (`False`). If you leave it unset or pass `None`, the condition keeps its current status, and a new condition goes on the problem list. `background`| _string_| `false`| Background information about the diagnosis. `status`| _Status enum_| `false`| The current status of the diagnosis. Must be one of `AssessCommand.Status`. `narrative`| _string_| `false`| The narrative for the current assessment (max 2048 characters; values exceeding the limit raise a validation error instead of being truncated). `Status`| Value| Description ---|---|--- `IMPROVED`| `"improved"`| The condition has improved. `STABLE`| `"stable"`| The condition is stable. `DETERIORATED`| `"deteriorated"`| The condition has deteriorated. **Examples** : Assess a condition by ICD-10 code, whether or not the patient has it charted: ```python from datetime import date from canvas_sdk.commands import AssessCommand assess = AssessCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', icd10_code='M54.50', approximate_date_of_onset=date(2012, 1, 1), background='lifted heavy box', narrative='unable to sleep lately' ) ``` Record a visit-only diagnosis as an encounter diagnosis: ```python from canvas_sdk.commands import AssessCommand assess = AssessCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', icd10_code='J02.9', show_in_problem_list=False, narrative='sore throat for two days' ) ``` Assess a specific condition already on the chart by its id: ```python from canvas_sdk.commands import AssessCommand assess = AssessCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', condition_id='a1c2e3d4-5b6f-4a7c-8e9d-0f1a2b3c4d5e', background='started in 2012', status=AssessCommand.Status.STABLE, narrative='experiencing more pain lately' ) ``` **Validation** : Passing both `condition_id` and `icd10_code` fails validation when you `originate` or `edit` the command, with the error `Name the condition with either condition_id or icd10_code, not both`. This applies even when one of them is `None`, because the empty value would clear the other. Leave out the argument you aren't using. Canvas checks `icd10_code` against the ICD-10 terminology when it applies the effect. A code that matches no ICD-10 condition fails with the error `ICD-10 code not found`, and the command is neither created nor updated. `condition_id` must belong to the same patient as the note or command it is written to: the patient comes from `note_uuid` when you `originate` the command, and from the existing command when you `edit` one. A condition on another patient's chart — or an id that matches no condition at all — fails validation, and the command is neither created nor updated. This check is deferred when the target note (on `originate`) or command (on `edit`) is not yet persisted — for example, when a plugin creates the note and originates `AssessCommand`s against that same `note_uuid` in a single handler response. In that case the note's or command's patient cannot be resolved yet, so `condition_id` passes this validation. The patient-ownership check then runs later, once the command is applied and the note exists. Committing the command checks `condition_id` again. If the condition was entered in error after it was picked, or isn't on the patient's chart, the commit fails with the error `The condition picked in this Assess was entered in error or is not on this patient's chart. Pick the condition again.` * * * ### ChangeMedication Changes the sig on an active medication without issuing a new prescription. See Recording a medication for how it compares to the other medication commands. **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `medication_id`| _string_| `true`| The id of the [Medication](/sdk/data-medication/#medication) being changed. Must be an active medication on that patient's chart. `sig`| _string_| `false`| Administration details of the medication. `assessment_id`| _string_| `false`| The id (a UUID) of an [Assessment](/sdk/data-assessment/#assessment) made in the same note as this command. A cross-note reference or an unknown id is rejected on `originate` and `edit`. The link clears automatically if that Assessment is deleted or entered in error, or if [a move puts the two in different notes](/sdk/data-assessment/#commands-linked-to-an-assessment). **Example** : ```python from canvas_sdk.commands.commands.change_medication import ChangeMedicationCommand change_medication = ChangeMedicationCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', medication_id='f0a1b2c3-d4e5-4f6a-8b9c-0d1e2f3a4b5c', sig='two pills taken orally' ) ``` **Validation** : `medication_id` must belong to the same patient as the note or command it is written to: the patient comes from `note_uuid` when you `originate` the command, and from the existing command when you `edit` one. The medication must also be active. A medication on another patient's chart, an id that matches no medication, or an inactive medication fails validation, and the command is neither created nor updated. This check is deferred when the target note (on `originate`) or command (on `edit`) is not yet persisted — for example, when a plugin creates the note and originates the command in the same batch of handler effects. In that case the command's patient cannot be resolved yet, so `medication_id` passes this validation; the check then runs once the command is applied. A malformed `medication_id` fails at command construction, before any patient lookup, while a well-formed UUID passed as a string is accepted. * * * ### ChartSectionReview Records that a section of the patient's chart was reviewed during a visit. Originating the command snapshots the patient's active records in that section onto the note, along with the rendered text of those records as they read at the time of review — the same thing that happens when a user clicks **Review** on a chart section in the Canvas UI. Use it to attest to a review your plugin has already performed, such as reconciling medications from an external source. The command is always committed on origination, so there is no staged state to fill in and no need to pass `commit=True`. Read the resulting snapshot back with the [ChartSectionReview](/sdk/data-chart-section-review/#chartsectionreview) data model. > **Info:** This command supports `originate()` only since it is a read only command. **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `section`| _ChartSectionReviewCommand.Sections enum_| `true`| The chart section being reviewed. Required when instantiating the command. Must be one of `ChartSectionReviewCommand.Sections`. **Example** : ```python from canvas_sdk.commands import ChartSectionReviewCommand def compute(): medication_review = ChartSectionReviewCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", section=ChartSectionReviewCommand.Sections.MEDICATIONS, ) return [medication_review.originate()] ``` #### ChartSectionReviewCommand.Sections Member| Value| Chart section ---|---|--- `CONDITIONS`| `conditions`| Conditions `SURGICAL_HISTORY`| `surgical_history`| Surgical History `MEDICATIONS`| `medications`| Medications `FAMILY_HISTORY`| `family_histories`| Family Histories `ALLERGIES`| `allergies`| Allergies `IMMUNIZATIONS`| `immunizations`| Immunizations > **Warning:** The member name for family history differs between the command and the data model: the command uses `ChartSectionReviewCommand.Sections.FAMILY_HISTORY`, while the data model uses `ChartSectionReviewSection.FAMILY_HISTORIES`. Both carry the same value, `family_histories`. * * * ### CloseGoal **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `goal_id`| _int_| `true`| The `dbid` of the [Goal](/sdk/data-goal/#goal) being closed. Must be a goal on that patient's chart. `achievement_status`| _AchievementStatus enum_| `false`| The final achievement status of the goal. Must be one of `GoalCommand.AchievementStatus`. `progress`| _string_| `false`| A narrative about the patient's progress toward the goal. `assessment_id`| _string_| `false`| The id (a UUID) of an [Assessment](/sdk/data-assessment/#assessment) made in the same note as this command. A cross-note reference or an unknown id is rejected on `originate` and `edit`. The link clears automatically if that Assessment is deleted or entered in error, or if [a move puts the two in different notes](/sdk/data-assessment/#commands-linked-to-an-assessment). **Example** : ```python from canvas_sdk.commands import CloseGoalCommand, GoalCommand close_goal = CloseGoalCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", goal_id=12345, achievement_status=GoalCommand.AchievementStatus.ACHIEVED, progress="Patient has achieved the target weight goal of 150 lbs." ) ``` ### Diagnose Records an active condition and assesses it for the first time. See Recording a condition for how it compares to Assess, MedicalHistory and AddCondition. > **Warning:** Diagnose is planned for retirement in a future release, and `DiagnoseCommand` will be deprecated then. Use AssessCommand with `icd10_code` for new work: it records a new condition the same way, and reuses the patient's charted condition instead of adding a duplicate. Existing plugins that use `DiagnoseCommand` keep working for now. **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `icd10_code`| _string_| `true`| ICD-10 code of the condition being diagnosed. Search with the [ICD-10 condition endpoint](/sdk/utils/#get-icdcondition--icd-10-conditions). `background`| _string_| `false`| Background information about the diagnosis. `approximate_date_of_onset`| _datetime_| `false`| The approximate date the condition began. `today_assessment`| _string_| `false`| The narrative for the initial assessment of the condition (max length: 2048 characters). **Example** : ```python from canvas_sdk.commands import DiagnoseCommand from datetime import datetime diagnose = DiagnoseCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', icd10_code='M54.50', background='lifted heavy box', approximate_date_of_onset=datetime(2012, 1, 1), today_assessment='unable to sleep lately' ) ``` * * * ### FamilyHistory **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `family_history`| _string_ or _Coding_| `true`| A description of the family history being documented. Search with the [family-history endpoint](/sdk/utils/#get-snomedfamily-history--family-history-conditions). `relative`| _string_| `false`| A description of the relative (e.g., mother, uncle). Search with the [family-relation endpoint](/sdk/utils/#get-snomedfamily-relation--family-relationships). `note`| _string_| `false`| Additional notes or context about the family history (max length: 512 characters). **Coding Support** : The `family_history` parameter accepts either: - **String** : Searches for matching family history conditions and selects the first result. - **Coding object** : Allows structured or unstructured coding - Supported systems: `SNOMED`, `UNSTRUCTURED` - Required fields: `system`, `code` - Optional field: `display` The `relative` parameter also searches and selects the first result when a string is provided. Use specific terms (e.g., `"Paternal Grandfather"`, `"Maternal Grandfather"`) to avoid ambiguous matches. **Example** : ```python from canvas_sdk.commands import FamilyHistoryCommand from canvas_sdk.commands.constants import CodeSystems, Coding # Using a string (searches and takes the first result — may be ambiguous) family_history = FamilyHistoryCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", family_history="Diabetes Type 2", relative="Mother", note="Diagnosed at age 45" ) # Using a SNOMED code family_history_snomed = FamilyHistoryCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", family_history=Coding( system=CodeSystems.SNOMED, code="44054006", display="Diabetes Type 2" ), relative="Mother", note="Diagnosed at age 45" ) # Using unstructured (free text) family_history_unstructured = FamilyHistoryCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", family_history=Coding( system=CodeSystems.UNSTRUCTURED, code="Family history of heart disease" ), relative="Father" ) ``` * * * ### FollowUp **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `structured`| _boolean_| `false`| Whether the RFV is structured or not. Defaults to False. `requested_date`| _date_| `false`| The desired follow up date. `note_type_id`| _UUID (str)_| `false`| The desired type of appointment. See [NoteType](/sdk/data-note/#notetype). `coding`| _Coding_ or _UUID (str)_| `true` if structured=True| The coding for the structured RFV. Either a full Coding object (with `code`, `system`, `display`) or a UUID string referencing a verified coding record. If a Coding is provided, it is validated against existing [ReasonForVisitSettingCoding](/sdk/data-reason-for-visit/#reasonforvisitsettingcoding) records `comment`| _string_| `false`| Additional commentary on the RFV. `assessment_id`| _string_| `false`| The id (a UUID) of an [Assessment](/sdk/data-assessment/#assessment) made in the same note as this command. A cross-note reference or an unknown id is rejected on `originate` and `edit`. The link clears automatically if that Assessment is deleted or entered in error, or if [a move puts the two in different notes](/sdk/data-assessment/#commands-linked-to-an-assessment). **Example** : ```python from canvas_sdk.commands import FollowUpCommand from datetime import date structured = FollowUpCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', structured=True, requested_date=date(2025, 3, 2), note_type_id="d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f5a", coding={'code': '49727002', 'system': 'http://snomed.info/sct', 'display': 'Cough'}, comment='also wants to discuss treatment options' ) # Example with a UUID string referencing a Coding record structured2 = FollowUpCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', structured=True, requested_date=date(2025, 3, 2), note_type_id="d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f5a", coding="e2b1e1e3-3f52-4a0a-bb3a-123456789abc", # Must correspond to an existing coding record comment="Discuss treatment options" ) unstructured = FollowUpCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', requested_date=date(2025, 3, 2), note_type_id="d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f5a", comment='also wants to discuss treatment options' ) ``` * * * ### Goal **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `goal_statement`| _string_| `true`| Description of the goal. `start_date`| _datetime_| `false`| The date the goal begins. `due_date`| _datetime_| `false`| The date the goal is due. `achievement_status`| _AchievementStatus enum_| `false`| The current achievement status of the goal. `priority`| _Priority enum_| `false`| The priority of the goal. `progress`| _string_| `false`| A narrative about the patient's progress toward the goal. `assessment_id`| _string_| `false`| The id (a UUID) of an [Assessment](/sdk/data-assessment/#assessment) made in the same note as this command. A cross-note reference or an unknown id is rejected on `originate` and `edit`. The link clears automatically if that Assessment is deleted or entered in error, or if [a move puts the two in different notes](/sdk/data-assessment/#commands-linked-to-an-assessment). `AchievementStatus`| Value| Description ---|---|--- `IN_PROGRESS`| `"in-progress"`| The goal is being pursued. `IMPROVING`| `"improving"`| Progress toward the goal is improving. `WORSENING`| `"worsening"`| Progress toward the goal is worsening. `NO_CHANGE`| `"no-change"`| No change in progress toward the goal. `ACHIEVED`| `"achieved"`| The goal has been achieved. `SUSTAINING`| `"sustaining"`| The achieved goal is being sustained. `NOT_ACHIEVED`| `"not-achieved"`| The goal was not achieved. `NO_PROGRESS`| `"no-progress"`| No progress has been made toward the goal. `NOT_ATTAINABLE`| `"not-attainable"`| The goal is not attainable. `Priority`| Value| Description ---|---|--- `HIGH`| `"high-priority"`| High priority. `MEDIUM`| `"medium-priority"`| Medium priority. `LOW`| `"low-priority"`| Low priority. **Example** : ```python from canvas_sdk.commands import GoalCommand from datetime import datetime goal = GoalCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', goal_statement='Eat more healthy vegetables.', start_date=datetime(2024, 1, 1), due_date=datetime(2024, 12, 31), achievement_status=GoalCommand.AchievementStatus.IN_PROGRESS, priority=GoalCommand.Priority.HIGH, progress='patient is frequenting local farmers market to find healthy options' ) ``` * * * ### HistoryOfPresentIllness **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `narrative`| _string_| `true`| The narrative of the patient's history of present illness. **Example** : ```python from canvas_sdk.commands import HistoryOfPresentIllnessCommand hpi = HistoryOfPresentIllnessCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', narrative='presents with chronic back pain and headaches' ) ``` * * * ### ImagingOrder **Command-specific parameters** : Name| Type| Required to delegate / sign| Description ---|---|---|--- `image_code`| _string_| `true`| Code identifier of the imaging order. Search with the [imaging-codes endpoint](/sdk/utils/#searching-for-imaging-codes). `diagnosis_codes`| _list[string]_| `true`| ICD-10 Diagnosis codes justifying the imaging order. Search with the [ICD-10 condition endpoint](/sdk/utils/#get-icdcondition--icd-10-conditions). `priority`| _Priority enum_| `false`| Priority of the imaging order. Must be one of `ImagingOrderCommand.Priority`. `additional_details`| _string_| `false`| Additional details or instructions related to the imaging order (max length: 1024 characters). `service_provider`| _ServiceProvider_| `true`| Service provider of the imaging order. Search with the [contacts endpoint](/sdk/utils/#searching-for-contacts-and-service-providers). `comment`| _string_| `false`| Additional comments (max length: 1024 characters). `ordering_provider_key`| _string_| `true`| The [Staff](/sdk/data-staff/#staff) `id` of the provider ordering the imaging. `linked_items_urns`| _list[string]_| `false`| List of URNs for items linked to the imaging order command. `assessment_id`| _string_| `false`| The id (a UUID) of an [Assessment](/sdk/data-assessment/#assessment) made in the same note as this command. A cross-note reference or an unknown id is rejected on `originate` and `edit`. The link clears automatically if that Assessment is deleted or entered in error, or if [a move puts the two in different notes](/sdk/data-assessment/#commands-linked-to-an-assessment). Linking also adds the assessed condition's ICD-10 codes to `diagnosis_codes` unless they're already listed. **Command-specific actions** : Action Name| Available When| Description ---|---|--- `delegate_action`| command is staged| Delegates the order by creating a task. `sign_action`| command is staged| Signs the order, transitioning it from staged to committed state. `print_specialist`| command is committed| Prints the order using a specialist-focused template. `print_patient`| command is committed| Prints the order using a patient-friendly template. `fax`| command is committed| Transmits the order electronically via fax. **Enums and Types** : **`Priority`** Priority| Value| Description ---|---|--- `ROUTINE`| `"Routine"`| A routine order. `URGENT`| `"Urgent"`| An urgent order. `STAT`| `"STAT"`| A STAT (immediate) order. **Example** : ```python from canvas_sdk.commands import ImagingOrderCommand from canvas_sdk.commands.constants import ServiceProvider imaging_order = ImagingOrderCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", image_code="G0204", diagnosis_codes=["E119"], priority=ImagingOrderCommand.Priority.ROUTINE, comment="this is a comment", additional_details="more details", ordering_provider_key="b8a7c6d5-4e3f-4a2b-9c1d-0e8f7a6b5c4d", service_provider=ServiceProvider( first_name="Clinic", last_name="Imaging", practice_name="Clinic Imaging", specialty="radiology", business_address="Street Address", business_phone="1234569874", business_fax="1234569874" ), ) ``` * * * ### ImagingReview **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `report_ids`| _list[string]_| `true`| List of [ImagingReport](/sdk/data-imaging/#imagingreport) IDs to review. Must be reports on that patient's chart. `message_to_patient`| _string_| `false`| Message to communicate findings to the patient. `communication_method`| _ReportReviewCommunicationMethod enum_| `false`| Method for patient communication. Must be one of `ReportReviewCommunicationMethod`. `linked_items_urns`| _list[string]_| `false`| List of URNs for items linked to the review. `comment`| _string_| `false`| Internal comment about the review. **Example** : ```python from canvas_sdk.commands import ImagingReviewCommand from canvas_sdk.commands.commands.review import ReportReviewCommunicationMethod from canvas_sdk.v1.data import ImagingReport, Patient patient = Patient.objects.get(id="patient-id") # Get imaging reports to review imaging_reports = ImagingReport.objects.filter(patient=patient, review__isnull=True, review_mode='RR') report_ids = [str(report.id) for report in imaging_reports] imaging_review = ImagingReviewCommand( note_uuid="a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d", report_ids=report_ids, message_to_patient="Your imaging results show no abnormalities.", communication_method=ReportReviewCommunicationMethod.DELEGATED_CALL_CAN_LEAVE_MESSAGE, comment="All clear, no follow-up needed." ) ``` * * * ### ImmunizationStatement **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `cpt_code`| _string_ or _Coding_| `false`*| The CPT code for the immunization procedure. Used with CVX code to search against ontologies server for validation. Search with the [immunization endpoint](/sdk/utils/#get-cptimmunization--search-immunizations). `cvx_code`| _string_ or _Coding_| `false`*| The CVX code for the vaccine administered. Used with CPT code to search against ontologies server for validation. Search with the [immunization endpoint](/sdk/utils/#get-cptimmunization--search-immunizations). `unstructured`| _Coding_| `false`*| Free-text immunization description. `approximate_date`| _date_| `false`| The approximate date when the immunization was administered. `comments`| _string_| `false`| Additional comments about the immunization (max 255 characters). *Must provide either both `cpt_code` and `cvx_code` together, or `unstructured` alone (cannot mix structured and unstructured). **Coding Support** : The `cpt_code` and `cvx_code` parameters accept either: - **String** : Looks up the code in the respective system (CPT or CVX) - **Coding object** : Allows structured coding - `cpt_code` must use system: `CPT` - `cvx_code` must use system: `CVX` - Required fields: `system`, `code` - Optional field: `display` The `unstructured` parameter: - **Coding object** : For free-text immunizations - Required system: `UNSTRUCTURED` - Required fields: `system`, `code` - Optional field: `display` **Examples** : ```python from canvas_sdk.commands.commands.immunization_statement import ImmunizationStatementCommand from canvas_sdk.commands.constants import CodeSystems, Coding from datetime import date immunization_statement = ImmunizationStatementCommand( cpt_code="90724", cvx_code="88", approximate_date=date(2024, 1, 15), comments="Patient received influenza vaccine" ) # Using Coding objects for structured codes immunization_statement_coded = ImmunizationStatementCommand( cpt_code=Coding( system=CodeSystems.CPT, code="90724" ), cvx_code=Coding( system=CodeSystems.CVX, code="88" ), approximate_date=date(2024, 1, 15), comments="Patient received influenza vaccine" ) # Using unstructured (free text immunization) immunization_statement_unstructured = ImmunizationStatementCommand( unstructured=Coding( system=CodeSystems.UNSTRUCTURED, code="COVID-19 booster at pharmacy" ), approximate_date=date(2024, 1, 15) ) ``` * * * ### Immunize Records a vaccine **administered** during the visit, including the lot it came from. **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `vaccine_id`| _UUID_| `true`| The `id` of a [Vaccine](/sdk/data-vaccine/#vaccine) in this instance's catalog. Must be active. `lot_id`| _UUID_| `false`*| The `id` of a [VaccineLot](/sdk/data-vaccine/#vaccinelot) with doses on hand. `lot_number`| _string_| `false`*| A lot number this instance does not stock, recorded as free text (max 20 characters). `manufacturer`| _string_| `false`| The vaccine's manufacturer (max 100 characters). `expiration_date`| _date_| `false`| The lot's expiration date. `sig`| _string_| `false`| Directions, as free text - for example `"0.5 mL IM, left deltoid"` (max 75 characters). `consent_given`| _boolean_| `true`| Whether the patient consented after reviewing the Vaccine Information Statement. Must be `true` to commit. `given_by_id`| _string_| `true`| The `id` of the [Staff](/sdk/data-staff/#staff) member who administered the vaccine. Must be active. `assessment_id`| _string_| `false`| The id (a UUID) of an [Assessment](/sdk/data-assessment/#assessment) made in the same note as this command. A cross-note reference or an unknown id is rejected on `originate` and `edit`. The link clears automatically if that Assessment is deleted or entered in error, or if [a move puts the two in different notes](/sdk/data-assessment/#commands-linked-to-an-assessment). *`lot_id` and `lot_number` are mutually exclusive; supplying both raises an error. Either may be omitted. **Choosing a vaccine and lot** : Both are instance-specific data, so look them up rather than hard-coding identifiers. A vaccine is only selectable on a note if it is active and carries an active CPT charge. See [Vaccine](/sdk/data-vaccine/) for the query. **Manufacturer and expiration** : When you supply a `lot_id` and leave `manufacturer` or `expiration_date` unset, the command fills them in from the lot. Anything you set explicitly is used as-is - including an explicit `None`, which is treated as a deliberate choice to leave the field empty rather than as an omission. A `lot_number` is free text with no inventory record behind it, so nothing is derived from it; set `manufacturer` and `expiration_date` yourself if you want them recorded. **Example** : ```python from datetime import date from canvas_sdk.commands.commands.immunize import ImmunizeCommand from canvas_sdk.v1.data import Vaccine, VaccineLot vaccine = Vaccine.objects.filter(active=True, cvx_code="135").first() lot = VaccineLot.objects.filter(vaccine__id=vaccine.id, on_hand_inventory__gt=0).first() # manufacturer and expiration_date are taken from the lot immunize = ImmunizeCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", vaccine_id=vaccine.id, lot_id=lot.id, sig="0.5 mL IM, left deltoid", consent_given=True, given_by_id="b8a7c6d5-4e3f-4a2b-9c1d-0e8f7a6b5c4d", ) # A lot the instance does not stock: supply the details yourself immunize_unstocked = ImmunizeCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", vaccine_id=vaccine.id, lot_number="ABC-12345", manufacturer="Acme Vaccines", expiration_date=date(2028, 1, 31), sig="0.5 mL IM, left deltoid", consent_given=True, given_by_id="b8a7c6d5-4e3f-4a2b-9c1d-0e8f7a6b5c4d", ) ``` * * * ### Instruct **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `coding`| **Coding**| `true`| The SNOMED code or UNSTRUCTURED code that represents the instruction. Search SNOMED with the [instruction endpoint](/sdk/utils/#get-snomedinstruction--instructions). `comment`| _string_| `false`| Additional comments related to the instruction. `assessment_id`| _string_| `false`| The id (a UUID) of an [Assessment](/sdk/data-assessment/#assessment) made in the same note as this command. A cross-note reference or an unknown id is rejected on `originate` and `edit`. The link clears automatically if that Assessment is deleted or entered in error, or if [a move puts the two in different notes](/sdk/data-assessment/#commands-linked-to-an-assessment). The Educational Material command only supports a fixed set of selectable values and is not available in this commands module. For free-text patient education, use `InstructCommand` instead — its `UNSTRUCTURED` coding accepts arbitrary instruction text. **Example** : ```python from canvas_sdk.commands import InstructCommand from canvas_sdk.commands.constants import CodeSystems, Coding # SNOMED code InstructCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', coding=Coding(system=CodeSystems.SNOMED, code="65921008"), comment="To address mild dehydration symptoms" ) # UNSTRUCTURED code InstructCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', coding=Coding(system=CodeSystems.UNSTRUCTURED, code="Physical medicine neuromuscular training"), ) ``` * * * ### LabOrder The `LabOrderCommand` is used to initiate a lab order through the Canvas system. This command requires detailed information about the lab partner, the tests being ordered, and the provider placing the order. There is no dedicated endpoint for ordering labs or medications. Labs and prescriptions are ordered by creating `LabOrderCommand` and Prescribe commands in a note via the SDK, typically in response to an [event](/sdk/events/). Built-in validations ensure that: - The specified lab partner exists (whether provided by name or ID). - The ordered tests are available for the chosen lab partner. **Electronic ordering:** LabOrder commands support the `send()` method for electronic ordering of signed orders directly to lab partners. However, electronic ordering has additional requirements: - Only lab partners with electronic ordering enabled support the `send()` method. - The command must be committed/signed before it can be sent electronically. - The patient must have an address and phone number on file. - The ordering provider must have an NPI. **Command-specific parameters** : Name| Type| Required to send| Description ---|---|---|--- `lab_partner`| _string_| `true`| The [lab partner](/sdk/data-lab-partner-and-test/#labpartner) processing the order. Accepts either the lab partner's name or its unique identifier (ID). `tests_order_codes`| _list[string]_| `true`| A list of codes or IDs for the [tests](/sdk/data-lab-partner-and-test/#labpartnertest-attributes) being ordered. The system verifies that each provided value corresponds to an available test for the specified lab partner. `ordering_provider_key`| _string_| `false`| The [Staff](/sdk/data-staff/#staff) `id` of the provider ordering the tests. `diagnosis_codes`| _list[string]_| `false`| ICD-10 Diagnosis codes justifying the lab order. Search with the [ICD-10 condition endpoint](/sdk/utils/#get-icdcondition--icd-10-conditions). `fasting_required`| _boolean_| `false`| Indicates if fasting is required for the tests. `comment`| _string_| `false`| Additional comments related to the lab order (max length: 128 characters). `assessment_id`| _string_| `false`| The id (a UUID) of an [Assessment](/sdk/data-assessment/#assessment) made in the same note as this command. A cross-note reference or an unknown id is rejected on `originate` and `edit`. The link clears automatically if that Assessment is deleted or entered in error, or if [a move puts the two in different notes](/sdk/data-assessment/#commands-linked-to-an-assessment). Linking also adds the assessed condition's ICD-10 codes to `diagnosis_codes` unless they're already listed. **Command-specific actions** : Action Name| Available When| Description ---|---|--- `sign_send_action`| command is staged| Signs and immediately sends the order electronically to the lab partner. `send_action`| command is staged| Sends the order electronically to the chosen lab partner. `sign_action`| command is staged| Signs the order, transitioning it from staged to committed state. `print_requisition_form`| command is committed| Prints the order using a requisition-focused template for lab submission. `print_specimen_label`| command is committed| Prints the template using a specimen-focused template. `fax_requisition_form`| command is committed| Transmits the order electronically via fax. **ABN Workflow Actions** When the ABN (Advance Beneficiary Notice) workflow is enabled, additional actions become available: Action Name| Available When| Description ---|---|--- `send_abn_signed`| command is staged| Sends the order electronically after ABN requirements are met. `make_changes`| command is staged| Allows modifications to complete ABN requirements before sending. #### Validations - **Lab Partner Validation:** The system checks that the provided `lab_partner` (by name or ID) exists in the system. If no matching lab partner is found, a validation error is raised. - **Tests Order Codes Validation:** Each test code or ID in `tests_order_codes` is verified against the tests available for the specified lab partner. If one or more tests cannot be found, the error will indicate which codes or IDs are missing. **Example** : ```python from canvas_sdk.commands import LabOrderCommand from canvas_sdk.v1.data.lab import LabPartner, LabPartnerTest partner = LabPartner.objects.first() tests = [test.order_code for test in LabPartnerTest.objects.filter(lab_partner=partner)] LabOrderCommand( lab_partner=str(partner.id), tests_order_codes=tests, ordering_provider_key="b8a7c6d5-4e3f-4a2b-9c1d-0e8f7a6b5c4d", diagnosis_codes=["E119"], fasting_required=True, comment="Patient should fast for 8 hours before the test." ) ``` * * * ### LabReview **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `report_ids`| _list[string]_| `true`| List of [LabReport](/sdk/data-labs/#labreport) IDs to review. Must be reports on that patient's chart. `message_to_patient`| _string_| `false`| Message to communicate findings to the patient. `communication_method`| _ReportReviewCommunicationMethod enum_| `false`| Method for patient communication. Must be one of `ReportReviewCommunicationMethod`. `linked_items_urns`| _list[string]_| `false`| List of URNs for items linked to the review. `comment`| _string_| `false`| Internal comment about the review. **Example** : ```python from canvas_sdk.commands import LabReviewCommand from canvas_sdk.commands.commands.review import ReportReviewCommunicationMethod from canvas_sdk.v1.data import LabReport, Patient patient = Patient.objects.get(id="patient-id") # Get lab reports to review lab_reports = LabReport.objects.filter(patient=patient, review__isnull=True, review_mode='RR') report_ids = [str(report.id) for report in lab_reports] lab_review = LabReviewCommand( note_uuid="a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d", report_ids=report_ids, message_to_patient="Your lab results are within normal range.", communication_method=ReportReviewCommunicationMethod.DELEGATED_CALL_CAN_LEAVE_MESSAGE, comment="All values normal, no follow-up needed." ) ``` * * * ### MedicalHistory Backfills a condition the patient had in the past that is not active now. See Recording a condition for how it compares to Assess and AddCondition. **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `past_medical_history`| _string_| `true`| An ICD-10 code or description of the past medical condition. ICD-10 codes are strongly preferred (see note below). Search with the [ICD-10 condition endpoint](/sdk/utils/#get-icdcondition--icd-10-conditions). `approximate_start_date`| _date_| `false`| Approximate start date of the condition. `approximate_end_date`| _date_| `false`| Approximate end date of the condition. `show_on_condition_list`| _boolean_| `false`| Whether the condition should appear on the condition list. `comments`| _string_| `false`| Additional comments (max length: 1000 characters). **Important: Use ICD-10 codes for accurate matching.** The `past_medical_history` field searches for matching conditions and selects the first result. When a text description is provided, similar conditions may match first. To guarantee the correct condition, pass the ICD-10 code directly (e.g., `"I1010"`). **Example** : ```python from canvas_sdk.commands import MedicalHistoryCommand from datetime import date # Preferred: use the ICD-10 code for exact matching MedicalHistoryCommand( past_medical_history="I1010", # Resistant Hypertension approximate_start_date=date(2015, 1, 1), show_on_condition_list=True, comments="Controlled with medication." ) # Also works but may match a different condition if the description is ambiguous MedicalHistoryCommand( past_medical_history="Resistant Hypertension", approximate_start_date=date(2015, 1, 1), show_on_condition_list=True, comments="Controlled with medication." ) ``` * * * ### MedicationStatement Records a medication the patient reports taking, without prescribing it. See Recording a medication for how it compares to the other medication commands. **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `fdb_code`| _string_ or _Coding_| `true`| The [FDB code](/sdk/utils/#fdb_code) of the medication `sig`| _string_| `false`| Administration details of the medication (max length: 1000 characters). **Coding Support** : The `fdb_code` parameter accepts either: - **String (FDB code)** : Looks up the medication in the FDB system - **Coding object** : Allows structured or unstructured coding - Supported systems: `FDB`, `UNSTRUCTURED` - Required fields: `system`, `code` - Optional field: `display` **Converting an RxNorm code to an FDB code:** MedicationStatement requires an FDB code; it cannot be originated directly from an RxNorm RXCUI. If you have an RxNorm code, use the [ontologies service](/sdk/utils/#searching-for-medications) grouped-medication lookup, which returns a `med_medication_id` — that value is the FDB code: ```python from urllib.parse import urlencode from canvas_sdk.utils.http import ontologies_http # search for a specific RxNorm RXCUI response_json = ontologies_http.get_json(f"/fdb/grouped-medication/?{urlencode({'rxnorm_rxcui': 313782})}").json() ``` **Example** : ```python from canvas_sdk.commands import MedicationStatementCommand from canvas_sdk.commands.constants import CodeSystems, Coding # Using an FDB code string (recommended for FDB medications) medication_statement = MedicationStatementCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', fdb_code='198698', sig='two pills taken orally' ) # Using an FDB Coding object medication_statement_fdb = MedicationStatementCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', fdb_code=Coding( system=CodeSystems.FDB, code='198698', display='aspirin 81 mg oral tablet' ), sig='two pills taken orally' ) # Using unstructured (free text medication) medication_statement_unstructured = MedicationStatementCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', fdb_code=Coding( system=CodeSystems.UNSTRUCTURED, code='Herbal supplement for joint health' ) ) ``` * * * ### OrderDme Orders durable medical equipment (DME), such as a wheelchair, catheter, or shower chair, from a note and sends the order to a supplier. Order a product from your DME formulary with `dme_product_id`, or describe an off-formulary item with `free_text_item`. Admins manage the formulary in the Django Admin UI under **DME Products**. In a plugin, look up a product and its ID with the [DmeProduct](/sdk/data-dme/#dmeproduct) data model, as the example below does. **Command-specific parameters** : Name| Type| Required to sign| Description ---|---|---|--- `dme_product_id`| _string_| `true`, unless `free_text_item` is set| The `id` of an active [DmeProduct](/sdk/data-dme/#dmeproduct) in your DME formulary. Can't be combined with `free_text_item`. `free_text_item`| _string_| `true`, unless `dme_product_id` is set| Description of an off-formulary item (max length: 255 characters). Can't be combined with `dme_product_id`. `diagnosis_codes`| _list[string]_| `true`| ICD-10 Diagnosis codes justifying the order. Search with the [ICD-10 condition endpoint](/sdk/utils/#get-icdcondition--icd-10-conditions). `quantity`| _integer_| `true`| Number of units ordered. Must be at least 1. `length_of_need`| _string_| `false`| How long the patient needs the equipment, such as `"6 months"` or `"Lifetime"` (max length: 255 characters). Canvas prints this text on the order as written. `service_provider`| _ServiceProvider_| `false`| The supplier the order is sent to. Search with the [contacts endpoint](/sdk/utils/#searching-for-contacts-and-service-providers). `ordering_provider_key`| _string_| `true`| The [Staff](/sdk/data-staff/#staff) `id` of the provider ordering the equipment. The staff member must be active and able to prescribe. `assessment_id`| _string_| `false`| The id (a UUID) of an [Assessment](/sdk/data-assessment/#assessment) made in the same note as this command. A cross-note reference or an unknown id is rejected on `originate` and `edit`. The link clears automatically if that Assessment is deleted or entered in error, or if [a move puts the two in different notes](/sdk/data-assessment/#commands-linked-to-an-assessment). Linking also adds the assessed condition's ICD-10 codes to `diagnosis_codes` unless they're already listed. Setting both `dme_product_id` and `free_text_item`, a `quantity` below 1, or a `length_of_need` longer than 255 characters raises a validation error when you create the command object. Canvas leaves a field empty when its value doesn't resolve. A `dme_product_id` resolves only to an active formulary product, and an `ordering_provider_key` only to an active staff member. Diagnosis codes that don't match an ICD-10 condition are dropped. The command can't be signed until every required field has a value. Signing also fails when the ordering provider isn't an active prescriber, because the printed order names them as the signer. Signing saves the item's formulary name and code and the indications with the order, along with the patient's latest height and weight. Later changes to the formulary or the patient's vitals don't change the printed order. **Command-specific actions** : Action Name| Available When| Description ---|---|--- `sign_action`| command is staged| Signs the order, transitioning it from staged to committed state. `print_order`| command is committed| Prints the order as a requisition. Requires printing access. `fax`| command is committed| Faxes the order, prefilling the supplier's fax number when one is on file. Requires printing access. To filter or annotate the products, indications, suppliers, and ordering providers a user sees while filling in the command, handle the `PRE_SEARCH` and `POST_SEARCH` [events](/sdk/events/#order-dme-command) for the `ITEM`, `INDICATIONS`, `SEND_TO`, and `ORDERING_PROVIDER` fields, such as `ORDER_DME__ITEM__POST_SEARCH`. Suppliers you create with the [ServiceProvider effect](/sdk/effect-service-provider/) aren't searched in **Send to** automatically. To offer them there, see [Offering your own providers alongside the directory](/guides/customize-search-results/#offering-your-own-providers-alongside-the-directory). **Example** : ```python from canvas_sdk.commands import OrderDmeCommand from canvas_sdk.commands.constants import ServiceProvider from canvas_sdk.v1.data import DmeProduct supplier = ServiceProvider( first_name="Acme", last_name="Medical Supply", practice_name="Acme Medical Supply", specialty="Durable Medical Equipment", business_fax="5555550100", ) # A product from the DME formulary, looked up by its HCPCS code. # get() raises an error unless exactly one active product has this code. wheelchair = DmeProduct.objects.get(code="K0001", active=True) formulary_order = OrderDmeCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", dme_product_id=str(wheelchair.id), diagnosis_codes=["N186"], quantity=1, length_of_need="Lifetime", service_provider=supplier, ordering_provider_key="b8a7c6d5-4e3f-4a2b-9c1d-0e8f7a6b5c4d", ) # An off-formulary item free_text_order = OrderDmeCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", free_text_item="Bariatric shower chair", diagnosis_codes=["E6601"], quantity=1, length_of_need="12 months", service_provider=supplier, ordering_provider_key="b8a7c6d5-4e3f-4a2b-9c1d-0e8f7a6b5c4d", ) ``` * * * ### SurgicalHistory Records a past surgical procedure, coded in SNOMED. See Recording a condition for how it compares to the other condition commands. **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `past_surgical_history`| _string_ or _Coding_| `true`| A description of the past surgical procedure. Search with the [procedures endpoint](/sdk/utils/#get-snomedprocedures--surgical-history-procedures). `approximate_date`| _date_| `false`| Approximate date of the surgery. `comment`| _string_| `false`| Additional comments (max length: 1000 characters). **Coding Support** : The `past_surgical_history` parameter accepts either: - **String** : Searches for matching surgical procedures and selects the first result. - **Coding object** : Allows structured or unstructured coding - Supported systems: `SNOMED`, `UNSTRUCTURED` - Required fields: `system`, `code` - Optional field: `display` **Example** : ```python from canvas_sdk.commands import PastSurgicalHistoryCommand from canvas_sdk.commands.constants import CodeSystems, Coding from datetime import date # Using a string (searches and takes the first result) PastSurgicalHistoryCommand( past_surgical_history="Appendectomy", approximate_date=date(2008, 6, 15), comment="No complications reported." ) # Using a SNOMED code surgical_history_snomed = PastSurgicalHistoryCommand( past_surgical_history=Coding( system=CodeSystems.SNOMED, code="80146002", display="Appendectomy" ), approximate_date=date(2008, 6, 15), comment="No complications reported." ) # Using unstructured (free text) surgical_history_unstructured = PastSurgicalHistoryCommand( past_surgical_history=Coding( system=CodeSystems.UNSTRUCTURED, code="Minor outpatient procedure on left knee" ), approximate_date=date(2020, 3, 10) ) ``` * * * ### Perform **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `cpt_code`| _string_ or _Coding_| `true`| The CPT code of the procedure or action performed. Look it up in the [Charge Description Master](/sdk/data-charge-description-master/#chargedescriptionmaster). `notes`| _string_| `false`| Additional notes related to the performed procedure. `assessment_id`| _string_| `false`| The id (a UUID) of an [Assessment](/sdk/data-assessment/#assessment) made in the same note as this command. A cross-note reference or an unknown id is rejected on `originate` and `edit`. The link clears automatically if that Assessment is deleted or entered in error, or if [a move puts the two in different notes](/sdk/data-assessment/#commands-linked-to-an-assessment). **Coding Support** : The `cpt_code` parameter accepts either: - **String** : Searches for matching procedures - **Coding object** : Allows structured or unstructured coding - Supported systems: `CPT`, `UNSTRUCTURED` - Required fields: `system`, `code` - Optional field: `display` **Example** : ```python from canvas_sdk.commands import PerformCommand from canvas_sdk.commands.constants import CodeSystems, Coding # Using a string (searches for matching procedures) PerformCommand( cpt_code="99213", notes="Patient presented with a common cold." ) # Using a CPT code perform_cpt = PerformCommand( cpt_code=Coding( system=CodeSystems.CPT, code="99213", display="Office visit, established patient" ), notes="Annual wellness visit" ) # Using unstructured (free text) perform_unstructured = PerformCommand( cpt_code=Coding( system=CodeSystems.UNSTRUCTURED, code="Custom procedure performed" ), notes="Non-standard procedure documentation" ) ``` * * * ### Plan **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `narrative`| _string_| `true`| The narrative of the patient's plan. `assessment_id`| _string_| `false`| The id (a UUID) of an [Assessment](/sdk/data-assessment/#assessment) made in the same note as this command. A cross-note reference or an unknown id is rejected on `originate` and `edit`. The link clears automatically if that Assessment is deleted or entered in error, or if [a move puts the two in different notes](/sdk/data-assessment/#commands-linked-to-an-assessment). **Example** : ```python from canvas_sdk.commands import PlanCommand plan = PlanCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', narrative='will return in 2 weeks to check on pain management' ) ``` * * * ### POCLabTest The `POCLabTestCommand` is used to document the results of a Point-of-Care (POC) lab test performed in the clinic — distinct from `LabOrder` (which sends tests to an external lab partner) and `LabReview` (which reviews returned results). The command captures the template used, the indications, individual measured values, and a free-text remarks field. Built-in validations ensure that: - The provided `template` UUID resolves to an active POC [`LabReportTemplate`](/sdk/data-lab-report-template/#labreporttemplate). - Each `test_values` entry's `label` matches one of the template's [field labels](/sdk/data-lab-report-template/#labreporttemplatefield) (case-insensitive). **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `template`| _UUID | string_| `true`| The UUID of the active POC [`LabReportTemplate`](/sdk/data-lab-report-template/#labreporttemplate). Accepts UUID instances or UUID-formatted strings. `indications`| _list[string]_| `false`| ICD-10 diagnosis codes justifying the test. Search with the [ICD-10 condition endpoint](/sdk/utils/#get-icdcondition--icd-10-conditions). `test_values`| _list[TestValue]_| `false`| The measured values, each tagged with its template-field label. See `TestValue` below. `remarks`| _string (≤512)_| `false`| Free-text comments from the clinician. **Enums and Types** : **`TestValue`** A dataclass representing a single measured value within a POC lab test result. Attribute| Type| Description ---|---|--- `label`| _string_| The template field's label (must match a field on the template). `value`| _string_| The measured value (as a string). `TestValue.to_dict()` returns the `{"label": ..., "value": ...}` dict shape consumed by the runtime. **Helper methods** : - `set_test_value(label, value)` — Adds or replaces a test value by label. If a `TestValue` with the same `label` already exists on the command, it is replaced (so calling `set_test_value` twice with the same label leaves a single entry). #### Validations - **Template Validation:** The `template` UUID must resolve to a [`LabReportTemplate`](/sdk/data-lab-report-template/#labreporttemplate) that is `active=True` and `poc=True`. Templates from external lab partners or inactive templates are rejected. - **Test Values Validation:** Each `TestValue.label` must match (case-insensitive) the `label` of one of the resolved template's [fields](/sdk/data-lab-report-template/#labreporttemplatefield). Unknown labels cause a validation error. The valid labels are the `label` of each [`LabReportTemplateField`](/sdk/data-lab-report-template/#labreporttemplatefield) on the template's [`fields`](/sdk/data-lab-report-template/#labreporttemplate) relation: ```python from canvas_sdk.v1.data import LabReportTemplate template = LabReportTemplate.objects.active().point_of_care().first() valid_labels = [field.label for field in template.fields.all()] ``` **Example** : ```python from canvas_sdk.commands import POCLabTestCommand from canvas_sdk.commands.commands.poc_lab_test import TestValue from canvas_sdk.v1.data import LabReportTemplate template = LabReportTemplate.objects.active().point_of_care().first() command = POCLabTestCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", template=template.id, indications=["E11.9"], test_values=[ TestValue(label="pH", value="6.5"), TestValue(label="Glucose", value="120"), ], remarks="Sample collected mid-stream", ) # Or via the helper (overwrites by label): command.set_test_value("pH", "6.8") ``` * * * ### Prescribe Writes a new prescription for a medication the patient is not already on, and can send it to a pharmacy. See Recording a medication for how it compares to the other medication commands. **Electronic prescribing:** Prescribe commands support the `send()` method for electronic transmission of signed prescriptions. However, electronic prescribing has additional validations: - A pharmacy must be specified on the command before it can be sent. - The command must be committed/signed before it can be sent electronically. - The prescriber must have an SPI (Surescripts Prescriber Identifier) number on file, or the send is restricted with `eRx unavailable, prescriber missing SPI number`. SPI is a send requirement only: a prescriber without one can still review and sign the prescription. - For a controlled substance, the prescriber must be enrolled in EPCS, or the send is restricted with `eRx unavailable, prescriber not enrolled in EPCS`. - For a controlled substance (a medication with a DEA schedule), the patient's [sex at birth](/sdk/data-patient/#sexatbirth) must be male or female, or the send is restricted with `eRx unavailable, patient sex at birth must be male or female`. These validations apply to Refill and AdjustPrescription as well, and in the Canvas UI as well as through the SDK — in the UI a restricted prescription offers no send action at all. **Overriding the prescriber address:** By default, the prescriber address transmitted on the prescription is derived from the prescriber's primary practice location. For workflows where a provider works across multiple offices — for example white bagging, where the medication ships to the office where the patient is being seen — pass a `practice_location_override` to `send()` to use a specific practice location's address instead: ```python from canvas_sdk.commands import PrescribeCommand def compute(): existing_prescribe = PrescribeCommand(command_uuid='e32b85d9-ccb7-4e4f-a0e5-8783ed2d9528') return [existing_prescribe.send(practice_location_override='a1b2c3d4-e5f6-7890-abcd-ef1234567890')] ``` - `practice_location_override` is the `id` of a [PracticeLocation](/sdk/data-practicelocation/#practicelocation). When set, that location's business name, phone, fax, and street address replace the prescriber's default on the outgoing prescription. - If the id does not correspond to an existing practice location, the send raises an error rather than falling back to the default address. - The override applies only to `send()`-initiated (plugin-driven) prescriptions. It does not affect prescriptions a clinician sends from the charting UI. **Signing is not supported programmatically.** A Prescribe command cannot be committed (signed) via the SDK. Surescripts requires certain elements to be present in the UI when prescribing in order to remain certified, so the prescriber must physically sign the prescription in the UI. The sign and send actions were separated into distinct steps so that the **send** step can be performed programmatically, but only after the prescription has been manually signed in the UI. The [send all prescriptions](/sdk/example-send_all_prescriptions/) example plugin demonstrates the supported programmatic-send flow. **Command-specific parameters** : Name| Type| Required to review / send| Description ---|---|---|--- `fdb_code`| _string_| `false`*| The [FDB code](/sdk/utils/#fdb_code) of the medication. `compound_medication_id`| _string_| `false`*| The id of an existing [CompoundMedication](/sdk/data-compound-medication/#compoundmedication) to prescribe. `compound_medication_data`| `CompoundMedicationData`| `false`*| Data for creating a new compound medication inline. `icd10_codes`| _list[string]_| `false`| List of ICD-10 codes (maximum 2) associated with the prescription. Must be active [Conditions](/sdk/data-condition/#condition) on the patient's chart, including encounter diagnoses kept off the problem list. `sig`| _string_| `true`| Administration instructions/details of the medication. Up to 1000 characters — see Limits. `days_supply`| _integer_| `false`| Number of days the prescription is intended to cover. `quantity_to_dispense`| _Decimal | float | integer_| `true`| The amount of medication to dispense. Must be greater than zero — see Limits. `type_to_dispense`| _ClinicalQuantity_| `true`**| Information about the form or unit of the medication to dispense. Get the available quantities from the [medication search](/sdk/utils/#searching-for-medications)'s `clinical_quantities`. `refills`| _integer_| `true`| Number of refills allowed for the prescription. From 0 to 99 — see Limits. `substitutions`| _Substitutions enum_| `true`| Specifies whether substitutions (e.g., generic drugs) are allowed. `pharmacy`| _string_| `false`| The NCPDP ID of the pharmacy where the prescription should be sent. [Look it up via the pharmacy search](/sdk/utils/#searching-for-pharmacies). `prescriber_id`| _string_| `true`| The [Staff](/sdk/data-staff/#staff) id of the prescriber. `supervising_provider_id`| _string_| `false`| The [Staff](/sdk/data-staff/#staff) id of the supervising provider of the prescriber. `note_to_pharmacist`| _string_| `false`| Additional notes or instructions for the pharmacist. Up to 210 characters — see Limits. `assessment_id`| _string_| `false`| The id (a UUID) of an [Assessment](/sdk/data-assessment/#assessment) made in the same note as this command. A cross-note reference or an unknown id is rejected on `originate` and `edit`. The link clears automatically if that Assessment is deleted or entered in error, or if [a move puts the two in different notes](/sdk/data-assessment/#commands-linked-to-an-assessment). Linking also adds the assessed condition to `icd10_codes` unless it's already listed. A prescription holds at most two codes, so a link that would add a third is rejected. *Must provide exactly one of: fdb_code, compound_medication_id, or compound_medication_data **ClinicalQuantity is only required when `fdb_code` is provided. It is optional for compound medications. **Compound medications:** Compound medications have no FDB code, so they are referenced with `compound_medication_id` or `compound_medication_data` rather than `fdb_code`. Because a compound medication has no FDB code, the quantity-to-dispense format is not preselected — the same as the UI behavior — and must be chosen manually in the UI after the command is inserted. Do not supply `type_to_dispense` for compound medications. **Command-specific actions** : Action Name| Available When| Description ---|---|--- `sign_send_action`| command is in review| Signs and immediately sends the prescription electronically. `sign_action`| command is in review| Signs the prescription, transitioning it from staged to committed state. `print_action`| command is in review| Prints and commits the command. `make_changes`| command is in review| Allow users to revert the command to staged state and make changes. `send_action`| command is committed| Sends the prescription electronically. **Enums and Types** Substitutions| Value| Description ---|---|--- `ALLOWED`| `"allowed"`| Generic or substitute medications are permitted. `NOT_ALLOWED`| `"not_allowed"`| Only the prescribed brand is allowed. **CompoundMedicationData** : Data for creating a compound medication inline within a prescription. Field Name| Type| Description| Required ---|---|---|--- `formulation`| _string_| The compound medication formulation (max 105 characters)| `true` `potency_unit_code`| _[PotencyUnit](/sdk/data-compound-medication/#potencyunit) value_| The unit of measurement for the medication.| `true` `controlled_substance`| _[ControlledSubstanceSchedule](/sdk/data-compound-medication/#controlledsubstanceschedule) value_| The controlled substance schedule (`N` for none).| `true` `controlled_substance_ndc`| _string_| NDC for controlled substances (dashes removed)| `false`* `active`| _bool_| Whether the compound medication is active (default: true)| `false` *Required when controlled_substance is not "N" (None) **Examples** ** _Option 1: Standard Prescription (FDB Code)_** ```python from canvas_sdk.commands.constants import ClinicalQuantity from canvas_sdk.commands import PrescribeCommand prescription = PrescribeCommand( fdb_code="216092", icd10_codes=["R51"], sig="Take one tablet daily after meals", days_supply=30, quantity_to_dispense=30, type_to_dispense=ClinicalQuantity( representative_ndc="12843016128", ncpdp_quantity_qualifier_code="C48542" ), refills=3, substitutions=PrescribeCommand.Substitutions.ALLOWED, pharmacy="pharmacy_ncpdp_id", prescriber_id="a7c2e9d1-3b4f-4a6c-8e0d-5f1a2b3c4d5e", supervising_provider_id='c3d4e5f6-7a8b-4c9d-0e1f-2a3b4c5d6e7f', note_to_pharmacist="Please verify patient's insurance before processing." ) ``` **_Option 2: Existing Compound Medication (by ID)_** Note: `type_to_dispense` should not be provided for compound medications as this field will auto-populate in the command when it is inserted in the note ```python from canvas_sdk.commands.constants import ClinicalQuantity from canvas_sdk.commands import PrescribeCommand from canvas_sdk.v1.data.compound_medication import CompoundMedication as CompoundMedicationModel # Get an existing compound medication (let's assume it exists in the database) compound_med = CompoundMedicationModel.objects.filter( active=True, formulation="Testosterone 200mg/mL in Grapeseed Oil" ).first() prescription = PrescribeCommand( compound_medication_id=str(compound_med.id), icd10_codes=["R51"], sig="Take one tablet daily after meals", days_supply=30, quantity_to_dispense=30, refills=3, substitutions=PrescribeCommand.Substitutions.ALLOWED, pharmacy="pharmacy_ncpdp_id", prescriber_id="a7c2e9d1-3b4f-4a6c-8e0d-5f1a2b3c4d5e", supervising_provider_id='c3d4e5f6-7a8b-4c9d-0e1f-2a3b4c5d6e7f', note_to_pharmacist="Please verify patient's insurance before processing." ) ``` **_Option 3: Create New Compound Medication Inline_** ```python from canvas_sdk.commands.constants import ClinicalQuantity from canvas_sdk.commands.commands.prescribe import PrescribeCommand, CompoundMedicationData from canvas_sdk.v1.data.compound_medication import CompoundMedication compound_medication_data = CompoundMedicationData( formulation="Testosterone 200mg/mL in Grapeseed Oil", potency_unit_code=CompoundMedication.PotencyUnits.GRAM, controlled_substance=CompoundMedication.ControlledSubstanceOptions.SCHEDULE_III, controlled_substance_ndc="12345678901", active=True, ) prescription = PrescribeCommand( compound_medication_data=compound_medication_data, icd10_codes=["M79.3"], sig="Apply thin layer to affected area twice daily", days_supply=30, quantity_to_dispense=30, refills=3, substitutions=PrescribeCommand.Substitutions.ALLOWED, pharmacy="pharmacy_ncpdp_id", prescriber_id="a7c2e9d1-3b4f-4a6c-8e0d-5f1a2b3c4d5e", supervising_provider_id='c3d4e5f6-7a8b-4c9d-0e1f-2a3b4c5d6e7f', note_to_pharmacist="Please verify patient's insurance before processing." ) ``` **Validation Notes** - Medication Type Validation: Exactly one of fdb_code, compound_medication_id, or compound_medication_data must be provided - Compound Medication ID: When using compound_medication_id, the system validates that the compound medication exists - Compound Medication Data: When using compound_medication_data: - All required fields in the dataclass must be provided - If controlled substance is not "N" (None), then controlled_substance_ndc is required - The formulation is limited to 105 characters - Any dashes in the NDC are automatically removed - Before creating a new compound medication, the system checks if a compound with the same formulation and potency unit code already exists. If it does, it reuses the existing compound medication instead of creating a new one. - Potency Unit and Controlled Substance Values: Must use valid enum values from PotencyUnit and ControlledSubstanceSchedule **Limits** A prescription has to fit what can be transmitted to the pharmacy, so four fields are bounded. These apply to Refill and AdjustPrescription as well, which share the fields. Field| Limit ---|--- `sig`| 1000 characters `note_to_pharmacist`| 210 characters `refills`| 0 to 99 `quantity_to_dispense`| greater than 0 All four are checked when the command is turned into an effect, not when the field is set. Building a command up field by field therefore never fails part-way through, and `originate()` or `edit()` reports every value that is out of bounds at once: ```python from canvas_sdk.commands import PrescribeCommand def compute(): prescribe = PrescribeCommand(note_uuid='c4d1e4b8-6a5f-4b3a-9e2d-7f8a9b0c1d2e') # Neither assignment raises. prescribe.refills = 100 prescribe.quantity_to_dispense = 0 # This raises a validation error naming both values. return [prescribe.originate()] ``` * * * ### PhysicalExam **Note:** The PhysicalExamCommand is a subclass of the QuestionnaireCommand, so it supports all the questionnaire features. That includes recording responses either with the `answers` parameter or with the `questions` property and `add_response()` — see Recording responses. **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `questionnaire_id`| _string_| `true`| The id of the [Questionnaire](/sdk/data-questionnaire/#questionnaire) being answered by the patient. The questionnaire's use case in charting must be **Physical Exam** , and it must be set to originate in charting. `answers`| _list ofAnswer_| `false`| The responses to record, one per question. Defaults to an empty list. #### Toggle Questions Feature The PhysicalExamCommand and the ReviewOfSystemsCommand both support toggling questions on/off, so practitioners can enable or disable specific questions based on patient relevance. The methods, property, and behavior described here are identical for both commands. The following methods are available. In each, `question_id` is the [Question](/sdk/data-questionnaire/#question) `dbid` (an integer, accepted as `int` or `str`): **Methods** : Method| Parameters| Returns| Description ---|---|---|--- `is_question_enabled`| `question_id: str` or `int`| `bool`| Check if a specific question is enabled (not skipped). `set_question_enabled`| `question_id: str` or `int, enabled: bool`| `None`| Enable or disable a specific question. **Properties** : Property| Type| Description ---|---|--- `question_toggles`| `dict`| All current toggle states, mapping `question_id` → `enabled` — e.g. `{"12": True, "13": False, "14": True}`. **Example - Working with Existing Commands** : A common use case is retrieving existing PhysicalExam commands from a note and modifying their toggle states. Here's how to work with the Canvas SDK data objects: ```python from canvas_sdk.commands import PhysicalExamCommand from canvas_sdk.v1.data import Command, Note from logger import log # Get existing physical exam commands from a note note = Note.objects.get(id="ff287601-fff4-46c4-b21f-04760e88adf1") physical_exam_commands = Command.objects.filter( note=note, schema_key="exam" # Physical exam commands have schema_key "exam" ).all() effects = [] for command in physical_exam_commands: # The command.data contains the question responses and skip states # Example structure of command.data: # { # "questionnaire": {"value": "83d93454-25a9-404d-83a5-e0ed2ec3af00"}, # "question-12": "70", # Body length response # "question-13": None, # Head circumference (no response) # "skip-12": True, # Body length is enabled (counterintuitive: skip=True means enabled) # "skip-13": False, # Head circumference is disabled # } # Create a PhysicalExamCommand instance from the existing command exam = PhysicalExamCommand(command_uuid=str(command.id)) # The exam.questions property gives you access to all questions with their IDs log.info(f"Processing Physical Exam Command: {exam.command_uuid}") for question in exam.questions: # Each question object has an 'id' property with the question ID question_id = question.dbid if exam.is_question_enabled(question_id): log.info(f"Question {question_id} is enabled") # Check if there's a response in the command data question_key = f"question-{question_id}" if question_key in command.data: response = command.data[question_key] if response: log.info(f"Response: {response}") # Example: Enable all questions that have responses, disable those without for question in exam.questions: question_id = question.dbid question_key = f"question-{question_id}" # Check if question has a response in command.data has_response = question_key in command.data and command.data[question_key] if has_response: exam.set_question_enabled(question_id, True) else: # Optionally disable questions without responses exam.set_question_enabled(question_id, False) effects.append(exam.edit()) ``` **Example - Creating a New Physical Exam** : ```python from canvas_sdk.commands import PhysicalExamCommand # Create a new physical exam exam = PhysicalExamCommand( note_uuid='a229456f-c10d-4f85-a04e-e8675d4e56dd', questionnaire_id='83d93454-25a9-404d-83a5-e0ed2ec3af00', ) questions = exam.questions # Retrieve the list of questions # Returns: [ # Question( # self.name='question-12', # self.label='Body length (in)', # self.type='TXT', # self.options=[ResponseOption(self.dbid=38, self.name='Body length (in)', self.code='8306-3', self.value='')], # self.response=None # ), # Question( # self.name='question-13', # self.label='Head circumference (cm)', # self.type='TXT', self.options=[ResponseOption(self.dbid=39, self.name='Head circumference (cm)', self.code='8287-5', self.value='')], # self.response=None # ) # Check if a question is enabled if exam.is_question_enabled("12"): print("Body length question is enabled.") # Disable irrelevant questions exam.set_question_enabled("13", False) # Get all toggle states states = exam.question_toggles # Returns: {"12": True, "13": False, "14": True, ...}, where keys are question IDs and values are enabled states. # Working with existing exam - toggle states are preserved existing_exam = PhysicalExamCommand(command_uuid='d4e5f6a7-8b9c-4d0e-1f2a-3b4c5d6e7f80') # All previously set toggle states are automatically loaded ``` * * * ### Questionnaire #### Overview The `QuestionnaireCommand` is used to present a questionnaire to a patient and commit their responses to an interview. It requires the ID of the questionnaire **Automatic Questionnaire ID Loading** : When instantiating a QuestionnaireCommand with an existing `command_uuid`, the questionnaire_id will be automatically loaded from the database if not explicitly provided. This means you don't need to specify the questionnaire_id when working with existing commands. **Note on the scoring narrative:** Setting a `result` value on the command instance has no effect — it is ignored by the command methods (`originate`, `edit`, `commit`). The questionnaire scoring narrative is instead set through the [Create a Questionnaire Result](/sdk/effect-questionnaires/#creating-a-questionnaire-result) effect. Once set, the narrative can be read from the command's `data` attribute, displays in the UI command, and (if the questionnaire is configured to show in the Social Determinants section) displays there too. In addition to the basic parameters, this command records responses in either of two ways: - **The`answers` parameter** — you pass the responses in, one per question, and the command works out how to apply each one. Nothing in your code branches on a question's type. Use this when you already have the question and option ids. - **The`questions` property with `add_response()`** — you read the questionnaire's questions off the command and record a response on each question object. The keyword you pass differs by question type, so your code branches on it. Use this when you need to inspect the questions or their options at runtime to decide what to answer. Both arrive at the same result, and they can be combined. `answers` is applied when the command's effect is built: it replaces whatever was recorded on the questions it names, and leaves a response recorded with `add_response()` on any other question alone. `answers` is not itself carried in the effect. **Recording responses with`answers`** The `answers` parameter takes a list of `Answer` objects, one per question. Each names a question and the response it takes; the command looks up the question, dispatches on its type, and resolves an option id to the option itself. A question id that is not in the questionnaire, an option id the question does not offer, or a response the question's type does not allow raises a `ValueError` when the effect is built. **`Answer` fields**: Name| Type| Required| Description ---|---|---|--- `question_id`| _integer_| `true`| The [Question](/sdk/data-questionnaire/#question) `dbid`. `response`| _string_ , _integer_ , or _list ofSelection_| `true`| Text for a text question, a number for an integer question, a [ResponseOption](/sdk/data-questionnaire/#responseoption) `dbid` for a radio question, an ISO 8601 `YYYY-MM-DD` string for a date question, or a list of `Selection` objects for a checkbox question. A checkbox question is the only kind whose responses carry comments, and each of its selections carries its own — so a comment belongs to a `Selection` rather than to the answer as a whole. > **Warning:** A date answer given through `answers` must be a string. `response` accepts a string, an integer or a list of `Selection`, so a `datetime.date` is refused. The question's own `add_response(date=...)` is the path that takes a `datetime.date` or a `datetime.datetime`. **`Selection` fields**: Name| Type| Required| Description ---|---|---|--- `option_id`| _integer_| `true`| The [ResponseOption](/sdk/data-questionnaire/#responseoption) `dbid` to tick. `comment`| _string_| `false`| What this selection is qualified with. `selected`| _boolean_| `false`| Defaults to `true`. Set it to `false` to untick the option — one a payload says nothing about keeps the state it already had. **Recording responses with`questions` and `add_response()`** Retrieve the list of questions via the `questions` property and record responses for each question using the question object's `add_response()` method. Each question type enforces its expected response format: - **Text questions (TYPE_TEXT):** Accept a keyword argument `text` (a string). - **Integer questions (TYPE_INTEGER):** Accept a keyword argument `integer` (a value convertible to an integer; a non-convertible value raises an error). - **Radio questions (TYPE_RADIO):** Accept a keyword argument `option` (a `ResponseOption` instance); only one option may be selected. - **Checkbox questions (TYPE_CHECKBOX):** Accept a keyword argument `option` (a `ResponseOption` instance) along with an optional boolean `selected` (defaulting to True) and an optional string `comment`. Multiple responses can be recorded. - **Date questions (TYPE_DATE):** Accept a keyword argument `date` (a `datetime.date`, a `datetime.datetime` normalized to its date, or an ISO 8601 date string `YYYY-MM-DD`). The value is stored as a normalized `YYYY-MM-DD` string; a string carrying a time component, an unparseable string, or a wrong type raises an error. **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `questionnaire_id`| _string_| `true`| The id of the [Questionnaire](/sdk/data-questionnaire/#questionnaire) being answered by the patient. `answers`| _list ofAnswer_| `false`| The responses to record, one per question. Defaults to an empty list. **Example** — instantiating an empty questionnaire: ```python from canvas_sdk.commands import QuestionnaireCommand questionnaire = QuestionnaireCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', questionnaire_id='c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d' ) ``` #### Usage Example Below is an example that answers a questionnaire with `answers`. Each `Answer` names a question by its `dbid` and gives the response in the form that question takes, so nothing branches on the question's type: ```python from canvas_sdk.commands.commands.questionnaire import Answer, QuestionnaireCommand, Selection from canvas_sdk.effects import Effect from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data import Note, Questionnaire class MyHandler(BaseHandler): def compute(self) -> list[Effect]: questionnaire = Questionnaire.objects.filter(name="Exercise").first() note = Note.objects.last() command = QuestionnaireCommand( note_uuid=str(note.id), questionnaire_id=str(questionnaire.id), answers=[ # A text question. Answer(question_id=12, response="Thanks for all the fish"), # An integer question. Answer(question_id=13, response=42), # A radio question, answered with the id of one of its options. Answer(question_id=14, response=101), # A date question. Give the date as a string, not a datetime.date. Answer(question_id=15, response="2026-07-14"), # A checkbox question, answered with one Selection per option ticked. Answer( question_id=16, response=[ Selection(option_id=201), Selection(option_id=202, comment="Don't panic"), ], ), ], ) # The responses are included when the command is originated. return [command.originate()] ``` An option id that the question does not offer, or a question id that is not in the questionnaire, raises a `ValueError` rather than recording something the questionnaire does not define. Below is the same thing written the other way, retrieving the questions and adding responses to them based on their type: ```python from canvas_sdk.commands.commands.questionnaire import QuestionnaireCommand from canvas_sdk.commands.commands.questionnaire.question import ResponseOption from canvas_sdk.effects import Effect from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data import Note, Questionnaire class MyHandler(BaseHandler): def compute(self) -> list[Effect]: q = Questionnaire.objects.filter(name="Exercise").first() note = Note.objects.last() # Create a QuestionnaireCommand instance. command = QuestionnaireCommand(questionnaire_id=str(q.id)) command.note_uuid = str(note.id) # Alternatively you can just retrieve an existing questionnaire command, and only return an `edit` effect. # Retrieve the list of questions. questions = command.questions # Record responses for each question. for question in questions: if question.type == ResponseOption.TYPE_TEXT: # For text questions, pass a 'text' keyword argument. question.add_response(text=f"Thanks for all the fish") elif question.type == ResponseOption.TYPE_INTEGER: # For integer questions, pass an 'integer' keyword argument. question.add_response(integer=42) elif question.type == ResponseOption.TYPE_RADIO: # For radio questions, pass an 'option' keyword argument (a ResponseOption instance). first_option = question.options[0] question.add_response(option=first_option) elif question.type == ResponseOption.TYPE_CHECKBOX: # For checkbox questions, add responses with option, selected flag, and optionally a comment. first_option = question.options[0] last_option = question.options[-1] question.add_response(option=first_option, selected=True, comment="Don't panic") question.add_response(option=last_option, selected=True) elif question.type == ResponseOption.TYPE_DATE: # For date questions, pass a 'date' keyword argument. question.add_response(date="2026-01-15") # The responses are included when the command is originated. return [command.originate()] ``` #### Explanation - **Retrieving Questions:** The `questions` property returns a list of question objects created from the questionnaire's data. - **Recording Responses:** Either set `answers` and let the command resolve each response against its question, or record them one at a time. Each question object provides an `add_response()` method that enforces the correct response format: - For **TextQuestion** , you must pass a `text` parameter. - For **IntegerQuestion** , you must pass an `integer` parameter. - For **RadioQuestion** , you must pass an `option` parameter (a `ResponseOption` instance) that corresponds to one of the allowed options. - For **CheckboxQuestion** , you must pass an `option` parameter along with an optional `selected` flag (defaulting to True) and an optional `comment`. Multiple responses can be recorded for checkbox questions. - **Note for Checkboxes:** Only the responses explicitly provided in the command payload will be updated in the UI. If a checkbox response is already selected and is not sent as unselected in the payload, its state remains unchanged. - For **DateQuestion** , you must pass a `date` parameter (a `datetime.date`, a `datetime.datetime`, or an ISO 8601 date string), stored as a normalized `YYYY-MM-DD` string. - **Creating and editing:** responses recorded with `answers` or `add_response()` are included when the command is originated, so `[command.originate()]` inserts the questionnaire already filled in, and `command.originate(commit=True)` also commits it. To change the responses of a questionnaire command that is already in the note, set them on a command with the same `command_uuid` and return its `edit()`. * * * ### ReasonForVisit **Command-specific parameters** : Name| Type| Required| Description ---|---|---|--- `structured`| _boolean_| `false`| Whether the RFV is structured or not. Defaults to False. `coding`| _Coding_ or _UUID (str)_| `true` if structured=True| The coding for the structured RFV. Either a full Coding object (with `code`, `system`, `display`) or a UUID string referencing a verified coding record. If a Coding is provided, it is validated against existing [ReasonForVisitSettingCoding](/sdk/data-reason-for-visit/#reasonforvisitsettingcoding) records `comment`| _string_| `false`| Additional commentary on the RFV. **Example** : ```python from canvas_sdk.commands import ReasonForVisitCommand structured_rfv = ReasonForVisitCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', structured=True, coding={'code': '49727002', 'system': 'http://snomed.info/sct', 'display': 'Cough'}, comment='also wants to discuss treatment options' ) # Example with a UUID string referencing a Coding record structured_rfv2 = ReasonForVisitCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', structured=True, coding="e2b1e1e3-3f52-4a0a-bb3a-123456789abc", # Must correspond to an existing coding record comment="Discuss treatment options" ) unstructured_rfv = ReasonForVisitCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', comment='also wants to discuss treatment options' ) ``` ### Refer **Command-specific parameters** : Name| Type| Required to delegate / sign| Description ---|---|---|--- `service_provider`| _ServiceProvider_| `true`| The service provider associated with the referral command. Search with the [contacts endpoint](/sdk/utils/#searching-for-contacts-and-service-providers). `diagnosis_codes`| _list[string]_| `true`| A list of relevant ICD-10 Diagnosis. Search with the [ICD-10 condition endpoint](/sdk/utils/#get-icdcondition--icd-10-conditions). `clinical_question`| _ClinicalQuestion enum_| `true`| The clinical question prompting the referral. Must be one of `ReferCommand.ClinicalQuestion` `priority`| _Priority enum_| `false`| Priority of the imaging order. Must be one of `ReferCommand.Priority`. `notes_to_specialist`| _string_| `true`| Notes or additional information directed to the specialist. `include_visit_note`| _boolean_| `false`| Flag indicating whether the visit note should be included in the referral. `comment`| _string_| `false`| An optional comment providing further details about the referral. `linked_items_urns`| _list[string]_| `false`| List of URNs for items linked to the referral command. `assessment_id`| _string_| `false`| The id (a UUID) of an [Assessment](/sdk/data-assessment/#assessment) made in the same note as this command. A cross-note reference or an unknown id is rejected on `originate` and `edit`. The link clears automatically if that Assessment is deleted or entered in error, or if [a move puts the two in different notes](/sdk/data-assessment/#commands-linked-to-an-assessment). Linking also adds the assessed condition's ICD-10 codes to `diagnosis_codes` unless they're already listed. **Command-specific actions** : Action Name| Available When| Description ---|---|--- `delegate_action`| command is staged| Delegates the order by creating a task. `sign_action`| command is staged| Signs the order, transitioning it from staged to committed state. `print_specialist`| command is committed| Prints the order using a specialist-focused template. `print_patient`| command is committed| Prints the order using a patient-friendly template. `fax`| command is committed| Transmits the order electronically via fax. **Enums and Types** : **`Priority`** Priority| Value| Description ---|---|--- `ROUTINE`| `"Routine"`| A routine referral. `URGENT`| `"Urgent"`| An urgent referral. `STAT`| `"STAT"`| A STAT (immediate) referral. **`ClinicalQuestion`** Clinical Question| Value| Description ---|---|--- `COGNITIVE_ASSISTANCE`| `"Cognitive Assistance (Advice/Guidance)"`| Cognitive assistance (advice/guidance). `ASSISTANCE_WITH_ONGOING_MANAGEMENT`| `"Assistance with Ongoing Management"`| Assistance with ongoing management. `SPECIALIZED_INTERVENTION`| `"Specialized intervention"`| Specialized intervention. `DIAGNOSTIC_UNCERTAINTY`| `"Diagnostic Uncertainty"`| Diagnostic uncertainty. **Example** : ```python from canvas_sdk.commands import ReferCommand from canvas_sdk.commands.constants import ServiceProvider refer_command = ReferCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", diagnosis_codes=["E119"], priority=ReferCommand.Priority.ROUTINE, clinical_question=ReferCommand.ClinicalQuestion.DIAGNOSTIC_UNCERTAINTY, comment="this is a comment", notes_to_specialist="This is a note to specialist", include_visit_note=True, service_provider=ServiceProvider( first_name="Clinic", last_name="Acupuncture", practice_name="Clinic Acupuncture", specialty="Acupuncture", business_address="Street Address", business_phone="1234569874", business_fax="1234569874" ), ) ``` * * * ### Reference Embeds a diagnostic view in the note. A diagnostic view is a saved combination of lab tests and questionnaire codes configured on your instance; referencing one renders that patient's results for those codes as a timeseries inside the note, so a reviewer sees the trend without leaving the chart. The command renders as a read-only table. There are no fields for a user to fill in, so the diagnostic view has to be chosen by whatever inserts the command — a user can only commit or delete it, and enter it in error once committed. Unlike ChartSectionReview, it is not committed on origination: it stays staged until you pass `commit=True` to `originate()` or send a separate `commit()`. Read the committed reference back with the [Reference](/sdk/data-reference/#reference) data model. > **Warning:** The rendered name and table are derived from the diagnostic view when the command is originated, and are not recalculated afterwards. Pointing an existing command at a different diagnostic view with `edit()` leaves the previous view's name and table on display. To change the view, delete the command and originate a new one. **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `diagnostic_view_id`| _UUID_ or _string_| `true`| The id of the [DiagnosticView](/sdk/data-diagnostic-view/#diagnosticview) to embed. An id that does not match a diagnostic view on the instance is discarded, leaving the command with no view to render. **Example** : ```python from canvas_sdk.commands import ReferenceCommand from canvas_sdk.v1.data import DiagnosticView def compute(): a1c_view = DiagnosticView.objects.filter(name="Hemoglobin A1c").first() if not a1c_view: return [] reference = ReferenceCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", diagnostic_view_id=a1c_view.id, ) return [reference.originate(commit=True)] ``` * * * ### ReferralReview **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `report_ids`| _list[string]_| `true`| List of [ReferralReport](/sdk/data-referral/#referralreport) IDs to review. Must be reports on that patient's chart. `message_to_patient`| _string_| `false`| Message to communicate findings to the patient. `communication_method`| _ReportReviewCommunicationMethod enum_| `false`| Method for patient communication. Must be one of `ReportReviewCommunicationMethod`. `linked_items_urns`| _list[string]_| `false`| List of URNs for items linked to the review. `comment`| _string_| `false`| Internal comment about the review. **Example** : ```python from canvas_sdk.commands import ReferralReviewCommand from canvas_sdk.commands.commands.review import ReportReviewCommunicationMethod from canvas_sdk.v1.data import Patient, ReferralReport patient = Patient.objects.get(id="patient-id") # Get referral reports to review referral_reports = ReferralReport.objects.filter(patient=patient, review__isnull=True, review_mode='RR') report_ids = [str(report.id) for report in referral_reports] referral_review = ReferralReviewCommand( note_uuid="a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d", report_ids=report_ids, message_to_patient="Your referral has been reviewed and approved.", communication_method=ReportReviewCommunicationMethod.DELEGATED_CALL_CAN_LEAVE_MESSAGE, comment="Referral approved, patient notified." ) ``` * * * ### Refill Renews a prescription for a medication the patient is already on, and can send it to a pharmacy. See Recording a medication for how it compares to the other medication commands. **Command-specific parameters** : Check the Prescribe command for the parameters used in the Refill command. Refill supports `send()` under the same electronic prescribing validations. **Example** : ```python from canvas_sdk.commands import RefillCommand, PrescribeCommand from canvas_sdk.commands.constants import ClinicalQuantity RefillCommand( fdb_code="216092", icd10_codes=["R51"], sig="Take one tablet daily after meals", days_supply=30, quantity_to_dispense=30, type_to_dispense=ClinicalQuantity( representative_ndc="12843016128", ncpdp_quantity_qualifier_code="C48542" ), refills=3, substitutions=PrescribeCommand.Substitutions.ALLOWED, pharmacy="pharmacy_ncpdp_id", prescriber_id="a7c2e9d1-3b4f-4a6c-8e0d-5f1a2b3c4d5e", supervising_provider_id="c3d4e5f6-7a8b-4c9d-0e1f-2a3b4c5d6e7f", note_to_pharmacist="Please verify patient's insurance before processing." ) ``` * * * ### RemoveAllergy **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `allergy_id`| _string_| `true`| The id of the [AllergyIntolerance](/sdk/data-allergy-intolerance/#allergyintolerance) to remove. Must be an allergy already recorded on that patient's chart. `narrative`| _string_| `false`| Additional context or narrative for the removal (max length: 512 characters). **Example** : ```python from canvas_sdk.commands import RemoveAllergyCommand RemoveAllergyCommand( allergy_id="e5f6a7b8-9c0d-4e1f-a2b3-c4d5e6f7a8b9", narrative="Allergy no longer applies after reassessment." ) ``` * * * ### Remove Past Medical History **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `condition_id`| _string_| `true`| The id of the [Condition](/sdk/data-condition/#condition) being removed from the patient's past medical history. Must be a committed, resolved, non-surgical condition already recorded on that patient's chart, with no committed assessment against it. An assessment that was entered in error, or is not committed yet, does not block removal. `rationale`| _string_| `false`| Additional context or narrative for the removal (max length: 512 characters). Committing this command enters the target condition in error, removing it from the patient's conditions list. The Past Medical History command that originally recorded the entry stays committed and visible in the note. Entering this command in error reverses the removal, returning the entry to the patient's chart. Each removal is recorded as a [RemovePastMedicalHistoryEvent](/sdk/data-remove-past-medical-history-event/), which you can query from the data module. > **Info:** An entry with a committed assessment cannot be removed, because the assessment would be left describing nothing. The in-note picker leaves these entries out, and a plugin that names one gets a validation error: `Condition {id} has assessments recorded against it, so this command cannot remove it`. The rule also applies at commit, so an assessment committed after the removal was originated still blocks the commit. **Example** : ```python from canvas_sdk.commands import RemovePastMedicalHistoryCommand from canvas_sdk.v1.data import Condition patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" # Leave out entries with a committed assessment, which this command refuses. past_medical_history_entry = ( Condition.objects.for_patient(patient_id) .committed() .filter(clinical_status="resolved", surgical=False) .exclude(assessments__committer__isnull=False, assessments__entered_in_error__isnull=True) .first() ) RemovePastMedicalHistoryCommand( condition_id=past_medical_history_entry.id, rationale="Imported in error from the HIE feed.", note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", ) ``` * * * ### Resolve Condition Closes an active condition that is no longer relevant to track. See Updating or removing a condition for how it compares to the other condition commands. **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `condition_id`| _string_| `true`| The id of the [Condition](/sdk/data-condition/#condition) being resolved. Must be an **active** condition on that patient's chart — committed, not entered in error, and not already resolved. `show_in_condition_list`| _boolean_| `false`| Determines whether the condition remains visible in patient chart summary. `rationale`| _string_| `false`| Additional context. ```python from canvas_sdk.commands.commands.resolve_condition import ResolveConditionCommand from canvas_sdk.v1.data import Condition patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" patient_condition = Condition.objects.for_patient(patient_id).committed().active().first() ResolveConditionCommand( condition_id=patient_condition.id, show_in_condition_list=True, rationale="Additional notes.", note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", ) ``` * * * ### Review of Systems **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `questionnaire_id`| _string_| `true`| The id of the [Questionnaire](/sdk/data-questionnaire/#questionnaire) being answered by the patient. The questionnaire's use case in charting must be **Review of Systems** , and it must be set to originate in charting. `answers`| _list ofAnswer_| `false`| The responses to record, one per question. Defaults to an empty list. #### Toggle Questions Feature The ReviewOfSystemsCommand supports the same question-toggling functionality as the PhysicalExamCommand, allowing practitioners to enable or disable specific system-review questions based on patient relevance. The available methods (`is_question_enabled`, `set_question_enabled`) and the `question_toggles` property are documented once under the PhysicalExam Toggle Questions Feature — they behave identically here. **Example** : ```python from canvas_sdk.commands import ReviewOfSystemsCommand # Create a new review of systems ros = ReviewOfSystemsCommand( note_uuid='8a18931a-acd9-474b-9070-ccd6fd472313', questionnaire_id='ed92577b-a023-4370-bc85-2b57e8afc4d8', ) questions = ros.questions # Retrieve the list of questions # Returns: [ # Question( # self.name='question-14', # self.label='Recurrent fever or chills', # self.type='TXT', # self.options=[]], # self.response=None # ), # Question( # self.name='question-25', # self.label='Other', # self.type='TXT', self.options=[], # self.response=None # ) # Check if a question is enabled if ros.is_question_enabled("14"): print("Recurrent fever or chills question is enabled.") # Disable irrelevant questions ros.set_question_enabled("25", False) # Get all toggle states states = ros.question_toggles # Returns: {"14": True, "25": False, "26": True, ...}, where keys are question IDs and values are enabled states. # Working with existing ros - toggle states are preserved existing_ros = ReviewOfSystemsCommand(command_uuid='d4e5f6a7-8b9c-4d0e-1f2a-3b4c5d6e7f80') # All previously set toggle states are automatically loaded ``` **Note:** The ReviewOfSystemsCommand is a subclass of the QuestionnaireCommand, so it supports all the questionnaire features. That includes recording responses either with the `answers` parameter or with the `questions` property and `add_response()` — see Recording responses. * * * ### StopMedication Records that a patient is stopping a medication, along with the reason. See Recording a medication for how it compares to the other medication commands. **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `medication_id`| _string_| `true`| The id of the [Medication](/sdk/data-medication/#medication) being stopped. Must be a medication already recorded on that patient's chart. `rationale`| _string_| `false`| The reason for stopping the medication. `assessment_id`| _string_| `false`| The id (a UUID) of an [Assessment](/sdk/data-assessment/#assessment) made in the same note as this command. A cross-note reference or an unknown id is rejected on `originate` and `edit`. The link clears automatically if that Assessment is deleted or entered in error, or if [a move puts the two in different notes](/sdk/data-assessment/#commands-linked-to-an-assessment). **Example** : ```python from canvas_sdk.commands import StopMedicationCommand stop_medication = StopMedicationCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', medication_id='f0a1b2c3-d4e5-4f6a-8b9c-0d1e2f3a4b5c', rationale='In remission' ) ``` * * * ### StructuredAssessment **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `questionnaire_id`| _string_| `true`| The id of the [Questionnaire](/sdk/data-questionnaire/#questionnaire) being answered by the patient. The questionnaire's use case in charting must be **Structured Assessment** , and it must be set to originate in charting. `answers`| _list ofAnswer_| `false`| The responses to record, one per question. Defaults to an empty list. **Note:** The StructuredAssessmentCommand is a subclass of the QuestionnaireCommand, so it supports all the questionnaire features. That includes recording responses either with the `answers` parameter or with the `questions` property and `add_response()` — see Recording responses. **Example** : ```python from canvas_sdk.commands import StructuredAssessmentCommand questionnaire = StructuredAssessmentCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', questionnaire_id='c1a2b3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d' ) ``` * * * ### Task **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `title`| _string_| `true`| The title or summary of the task. `assign_to`| _TaskAssigner_| `true`| Specifies the assignee (role, team, or individual). `due_date`| _date_| `false`| Due date for completing the task. `priority`| _TaskPriority enum_| `false`| Priority of the task. Must be one of `TaskPriority`. `comment`| _string_| `false`| Additional comments or notes about the task. `labels`| _list[string]_| `false`| Labels to apply to the task. Each value is matched (case-insensitive) against an existing [TaskLabel](/sdk/data-task/#tasklabel) by name; values that don't match an existing label are ignored. `linked_items_urns`| _list[string]_| `false`| URNs for items linked to the task. `assessment_id`| _string_| `false`| The id (a UUID) of an [Assessment](/sdk/data-assessment/#assessment) made in the same note as this command. A cross-note reference or an unknown id is rejected on `originate` and `edit`. The link clears automatically if that Assessment is deleted or entered in error, or if [a move puts the two in different notes](/sdk/data-assessment/#commands-linked-to-an-assessment). **Enums and Types** : **`TaskPriority`** Priority| Description ---|--- `STAT`| The request should be actioned immediately — highest possible priority. E.g. an emergency. `URGENT`| The request should be actioned promptly — higher priority than routine. `ROUTINE`| The request has normal priority. **TaskAssigner Type** : Key| Type| Required| Description ---|---|---|--- `to`| _AssigneeType_| `true`| Type of assignee (e.g., role, team, etc.). `id`| _integer_| `false`| The `dbid` of the assignee, in the table selected by `to`: a [CareTeamRole](/sdk/data-care-team/#careteamrole) when `to` is `ROLE`, a [Team](/sdk/data-team/#team) when `to` is `TEAM`, or a [Staff](/sdk/data-staff/#staff) when `to` is `STAFF`. Omit when `to` is `UNASSIGNED`. AssigneeType| Value| Description ---|---|--- `ROLE`| `"role"`| Task assigned to a specific [CareTeamRole](/sdk/data-care-team/#careteamrole) (`id` is the role's `dbid`). `TEAM`| `"team"`| Task assigned to a specific [Team](/sdk/data-team/#team) (`id` is the team's `dbid`). `UNASSIGNED`| `"unassigned"`| Task is unassigned. `STAFF`| `"staff"`| Task assigned to a specific [Staff](/sdk/data-staff/#staff) member (`id` is the staff member's `dbid`). **Example** : ```python from canvas_sdk.commands import TaskCommand from canvas_sdk.commands.commands.task import TaskAssigner, AssigneeType from canvas_sdk.v1.data.task import TaskPriority from datetime import date TaskCommand( title="Follow-up appointment scheduling", assign_to=TaskAssigner(to=AssigneeType.STAFF, id=123), due_date=date(2024, 12, 15), priority=TaskPriority.URGENT, comment="Ensure the patient schedules a follow-up within 30 days.", labels=["Urgent"], linked_items_urns=["urn:task:123", "urn:note:456"] ) ``` * * * ### UncategorizedDocumentReview **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `report_ids`| _list[string]_| `true`| List of [UncategorizedClinicalDocument](/sdk/data-uncategorized-clinical-document/#uncategorizedclinicaldocument) ids to review. Must be documents already on that patient's chart. `message_to_patient`| _string_| `false`| Message to communicate findings to the patient. `communication_method`| _ReportReviewCommunicationMethod enum_| `false`| Method for patient communication. Must be one of `ReportReviewCommunicationMethod`. `linked_items_urns`| _list[string]_| `false`| List of URNs for items linked to the review. `comment`| _string_| `false`| Internal comment about the review. **Example** : ```python from canvas_sdk.commands import UncategorizedDocumentReviewCommand from canvas_sdk.v1.data import UncategorizedClinicalDocument, Patient from canvas_sdk.commands.commands.review import ReportReviewCommunicationMethod patient = Patient.objects.last() # Get uncategorized documents to review uncategorized_documents = UncategorizedClinicalDocument.objects.filter(patient=patient, review__isnull=True, review_mode='RR') report_ids = [str(doc.id) for doc in uncategorized_documents] uncategorized_review = UncategorizedDocumentReviewCommand( note_uuid="a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d", report_ids=report_ids, message_to_patient="Your document has been reviewed.", communication_method=ReportReviewCommunicationMethod.DELEGATED_CALL_CAN_LEAVE_MESSAGE, comment="Document reviewed, no further action needed." ) ``` * * * ### UpdateDiagnosis Replaces an existing diagnosis with a different code, carrying the original's history onto the new condition. See Updating or removing a condition for how it compares to the other condition commands. **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `condition_code`| _string_| `true`| The ICD-10 code of the existing diagnosis to update. Must match an active [Condition](/sdk/data-condition/#condition) on that patient's chart, including encounter diagnoses recorded with Assess's `show_in_problem_list=False`. `new_condition_code`| _string_| `true`| The new ICD-10 code to replace the existing diagnosis, looked up via [`GET /icd/condition/`](/sdk/utils/#get-icdcondition--icd-10-conditions). `background`| _string_| `false`| Background information or notes related to the updated diagnosis (max length: 2048 characters). `narrative`| _string_| `false`| A narrative or explanation about the update (max length: 2048 characters). * * * **Example** ```python from canvas_sdk.commands import UpdateDiagnosisCommand UpdateDiagnosisCommand( condition_code="E119", new_condition_code="E109", background="Patient previously diagnosed with diabetes type 2; now updated to diabetes type 1.", narrative="Updating condition based on recent clinical findings." ) ``` * * * ### UpdateGoal **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `goal_id`| _string_| `true`| The `id` of the [Goal](/sdk/data-goal/#goal) being updated. Must be a goal on that patient's chart. `due_date`| _datetime_| `false`| The date the goal is due. `achievement_status`| _AchievementStatus enum_| `false`| The current achievement status of the goal. `priority`| _Priority enum_| `false`| The priority of the goal. `progress`| _string_| `false`| A narrative about the patient's progress toward the goal. `assessment_id`| _string_| `false`| The id (a UUID) of an [Assessment](/sdk/data-assessment/#assessment) made in the same note as this command. A cross-note reference or an unknown id is rejected on `originate` and `edit`. The link clears automatically if that Assessment is deleted or entered in error, or if [a move puts the two in different notes](/sdk/data-assessment/#commands-linked-to-an-assessment). `AchievementStatus`| Value| Description ---|---|--- `IN_PROGRESS`| `"in-progress"`| The goal is being pursued. `IMPROVING`| `"improving"`| Progress toward the goal is improving. `WORSENING`| `"worsening"`| Progress toward the goal is worsening. `NO_CHANGE`| `"no-change"`| No change in progress toward the goal. `ACHIEVED`| `"achieved"`| The goal has been achieved. `SUSTAINING`| `"sustaining"`| The achieved goal is being sustained. `NOT_ACHIEVED`| `"not-achieved"`| The goal was not achieved. `NO_PROGRESS`| `"no-progress"`| No progress has been made toward the goal. `NOT_ATTAINABLE`| `"not-attainable"`| The goal is not attainable. `Priority`| Value| Description ---|---|--- `HIGH`| `"high-priority"`| High priority. `MEDIUM`| `"medium-priority"`| Medium priority. `LOW`| `"low-priority"`| Low priority. **Example** : ```python from canvas_sdk.commands import UpdateGoalCommand, GoalCommand from datetime import datetime update_goal = UpdateGoalCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', goal_id='b7c8d9e0-1a2b-4c3d-5e6f-7a8b9c0d1e2f', due_date=datetime(2025, 3, 31), achievement_status=GoalCommand.AchievementStatus.WORSENING, priority=GoalCommand.Priority.MEDIUM, progress='patient has slowed down progress and requesting to move due date out' ) ``` * * * ### VisualExamFinding Records a titled image of a clinical observation, with a narrative, on a note. The Visual Exam Finding command must be enabled on the instance to attach an image. **Command-specific parameters** : Name| Type| Description ---|---|--- `title`| _string_| The title of the finding. Up to 255 characters. `narrative`| _string_| A description of the observation. `image_upload_key`| _string_| The upload key of a PNG or JPEG image, from a SimpleAPI route declared with `file_uploads="stored"`. See [Storing uploaded files](/sdk/handlers-simple-api-http/#storing-uploaded-files). Canvas copies the file to the finding and then deletes the upload, so each key works only once. A key of `None` attaches no image. Not allowed in a recommendation, such as one on a protocol card. The command supports `originate`, `edit`, `delete`, `commit`, and `enter_in_error`. To read findings, see [VisualExamFinding](/sdk/data-visual-exam-finding/). **Example** : ```python from canvas_sdk.commands import VisualExamFindingCommand finding = VisualExamFindingCommand( note_uuid='8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47', title='Rash', narrative='Erythematous rash on the left forearm', image_upload_key='acme/plugin-uploads/my_plugin/20261014T120000Z-abc-rash.jpg', ) effect = finding.originate() ``` In a SimpleAPI handler, set `image_upload_key` to the `key` of the uploaded `StoredFilePart` rather than a literal string. **Example with an uploaded photo** : This SimpleAPI route adds a Visual Exam Finding to a note from a `multipart/form-data` request that carries the note's id, a title, a narrative, and the photo. The route uses the [API key mixin](/sdk/handlers-simple-api-http/#api-key-1), so declare the `simpleapi-api-key` secret in your manifest and set it on your instance: ```python from http import HTTPStatus from canvas_sdk.commands import VisualExamFindingCommand from canvas_sdk.effects import Effect from canvas_sdk.effects.simple_api import JSONResponse, Response from canvas_sdk.handlers.simple_api import APIKeyAuthMixin, SimpleAPI, api class VisualExamFindingAPI(APIKeyAuthMixin, SimpleAPI): @api.post("/visual-exam-finding", file_uploads="stored") def visual_exam_finding(self) -> list[Response | Effect]: form = self.request.form_data() image = form["image"] if image.error: return [JSONResponse({"error": "upload failed"}, status_code=HTTPStatus.BAD_GATEWAY)] finding = VisualExamFindingCommand( note_uuid=form["note_id"].value, title=form["title"].value, narrative=form["narrative"].value, image_upload_key=image.key, ) return [finding.originate(), JSONResponse({}, status_code=HTTPStatus.ACCEPTED)] ``` Call the route with a `multipart/form-data` POST, sending the photo as a file and the API key in the `Authorization` header: ```shell curl --request POST \ --url 'https://.canvasmedical.com/plugin-io/api//visual-exam-finding' \ --header 'Authorization: ' \ --form 'note_id=8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47' \ --form 'title=Rash' \ --form 'narrative=Erythematous rash on the left forearm' \ --form 'image=@rash.jpg;type=image/jpeg' ``` The same request from a Python script, uploading a photo saved on disk, using the [`requests`](https://requests.readthedocs.io/) library: ```python from pathlib import Path import requests url = "https://.canvasmedical.com/plugin-io/api//visual-exam-finding" image_path = Path("rash.jpg") with image_path.open("rb") as image: response = requests.post( url, headers={"Authorization": ""}, data={ "note_id": "8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", "title": "Rash", "narrative": "Erythematous rash on the left forearm", }, files={"image": (image_path.name, image, "image/jpeg")}, timeout=30, ) print(response.status_code) ``` * * * ### Vitals **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `height`| _integer_| `false`| Height in inches. `weight_lbs`| _integer_| `false`| Weight in pounds. `weight_oz`| _integer_| `false`| Weight in ounces. `waist_circumference`| _integer_| `false`| Waist circumference in inches. `body_temperature`| _float_| `false`| Body temperature in Fahrenheit. `body_temperature_site`| _BodyTemperatureSite_| `false`| Site of body temperature measurement. `blood_pressure_systole`| _integer_| `false`| Systolic blood pressure. `blood_pressure_diastole`| _integer_| `false`| Diastolic blood pressure. `blood_pressure_position_and_site`| _BloodPressureSite_| `false`| Position and site of blood pressure measurement. `pulse`| _integer_| `false`| Pulse rate in beats per minute. `pulse_rhythm`| _PulseRhythm_| `false`| Rhythm of the pulse. `respiration_rate`| _integer_| `false`| Respiration rate in breaths per minute. `oxygen_saturation`| _integer_| `false`| Oxygen saturation in percentage. `supplemental_oxygen`| _SupplementalOxygen_| `false`| Type of supplemental oxygen the patient is receiving. `note`| _string_| `false`| Additional notes (max length: 150 characters). For an example of how weight (`weight_lbs` / `weight_oz`) and other vitals values are stored and read back via the SDK, see the [vitals visualizer](https://github.com/canvas-medical/canvas-plugins/tree/main/example-plugins/vitals_visualizer_plugin) example plugin, which retrieves a patient's weight and displays it in a side modal. **Enums and Types** : BodyTemperatureSite| Value| Description ---|---|--- `AXILLARY`| `0`| Measurement taken from the armpit. `ORAL`| `1`| Measurement taken from the mouth. `RECTAL`| `2`| Measurement taken from the rectum. `TEMPORAL`| `3`| Measurement taken from the forehead. `TYMPANIC`| `4`| Measurement taken from the ear. BloodPressureSite| Value| Description ---|---|--- `SITTING_RIGHT_UPPER`| `0`| Sitting position, right upper arm. `SITTING_LEFT_UPPER`| `1`| Sitting position, left upper arm. `STANDING_RIGHT_UPPER`| `4`| Standing position, right upper arm. `SUPINE_LEFT_LOWER`| `11`| Supine position, left lower arm. PulseRhythm| Value| Description ---|---|--- `REGULAR`| `0`| Regular rhythm. `IRREGULARLY_IRREGULAR`| `1`| Completely irregular rhythm. `REGULARLY_IRREGULAR`| `2`| Regularly irregular rhythm. SupplementalOxygen| Value| Description ---|---|--- `CONTINUOUS_HIGH_FLOW`| `"LA28684-1"`| Continuous high-flow supplemental oxygen. `CONTINUOUS_LOW_FLOW`| `"LA28685-8"`| Continuous low-flow supplemental oxygen. `INTERMITTENT`| `"LA28686-6"`| Intermittent supplemental oxygen. **Example** : ```python from canvas_sdk.commands import VitalsCommand VitalsCommand( height=70, weight_lbs=150, body_temperature=98, body_temperature_site=VitalsCommand.BodyTemperatureSite.ORAL, blood_pressure_systole=120, blood_pressure_diastole=80, blood_pressure_position_and_site=VitalsCommand.BloodPressureSite.SITTING_RIGHT_UPPER, pulse=72, pulse_rhythm=VitalsCommand.PulseRhythm.REGULAR, oxygen_saturation=98, supplemental_oxygen=VitalsCommand.SupplementalOxygen.INTERMITTENT, note="Vitals are within normal range." ) ``` ## Command Constants The `canvas_sdk.commands.constants` module provides essential classes and enumerations used across various Canvas SDK command implementations. These constants ensure consistency and provide structured data types for common medical and administrative elements. ### ClinicalQuantity `ClinicalQuantity` represents detailed information about the form or unit of medication, particularly for prescription-related commands. Field Name| Type| Required| Description ---|---|---|--- `representative_ndc`| _string_| `true`| National Drug Code (NDC) representing the medication. `ncpdp_quantity_qualifier_code`| _string_| `true`| NCPDP code indicating the quantity qualifier. `description`| _string_| `false`| The clinical quantity description to dispense (e.g. `"0.5 mL vial"`). Use this field to narrow the selection to the correct clinical quantity when multiple options are available for the same NDC and qualifier code. If omitted, the first available clinical quantity is used. These values come from the `clinical_quantities` array returned by the [medication search](/sdk/utils/#searching-for-medications): `representative_ndc` ← `representative_ndc`, `ncpdp_quantity_qualifier_code` ← `erx_ncpdp_script_quantity_qualifier_code`, and `description` ← `clinical_quantity_description`. **Usage Example** : ```python from canvas_sdk.commands import PrescribeCommand from canvas_sdk.commands.constants import ClinicalQuantity # Without description — selects the first available clinical quantity clinical_quantity = ClinicalQuantity( representative_ndc="12843016128", ncpdp_quantity_qualifier_code="C48542" ) # With description — narrows to the correct clinical quantity when multiple options share the same NDC and qualifier code clinical_quantity = ClinicalQuantity( representative_ndc="00002024304", ncpdp_quantity_qualifier_code="C28254", description="0.5 mL vial" ) prescribe = PrescribeCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", fdb_code="216092", icd10_codes=["R51"], sig="Take one tablet daily after meals", days_supply=30, quantity_to_dispense=30, type_to_dispense=clinical_quantity, refills=3, substitutions=PrescribeCommand.Substitutions.ALLOWED ) ``` **Looking up`representative_ndc` and `ncpdp_quantity_qualifier_code`:** When a Prescribe command uses an `fdb_code`, `type_to_dispense` requires a `ClinicalQuantity` with both `representative_ndc` and `ncpdp_quantity_qualifier_code`. You can obtain these values from the [ontologies service](/sdk/utils/#making-requests-to-the-ontologies-service). The FDB code is the `med_medication_id` returned by a medication search; query the grouped-medication endpoint directly by that ID to read its clinical quantities: ```python from canvas_sdk.utils.http import ontologies_http med_medication_id = 436095 # this is the fdb_code response_json = ontologies_http.get_json(f"/fdb/grouped-medication/{med_medication_id}/").json() ``` A single FDB code can contain multiple `clinical_quantities`, so filter to the correct one by matching its `clinical_quantity_description` to your medication's form. The fields map to `ClinicalQuantity` as follows: `representative_ndc` → `representative_ndc`, and `erx_ncpdp_script_quantity_qualifier_code` → `ncpdp_quantity_qualifier_code`. ### ServiceProvider `ServiceProvider` represents detailed information about healthcare service providers, used in referral, imaging order, and Order DME commands. Field Name| Type| Description ---|---|--- `first_name`| _string_| Service provider's first name (max length 512) `last_name`| _string_| Service provider's last name (max length 512) `specialty`| _string_| Provider's specialty (max length 512) `practice_name`| _string_| Name of the practice (max length 512) `business_fax`| _Optional[string]_| Business fax number (optional, max length 512) `business_phone`| _Optional[string]_| Business phone number (optional, max length 512) `business_address`| _Optional[string]_| Business address (optional, max length 512) `notes`| _Optional[string]_| Additional notes (optional, max length 512) **Usage Example** : ```python from canvas_sdk.commands import ReferCommand from canvas_sdk.commands.constants import ServiceProvider # Creating a referral with service provider information service_provider = ServiceProvider( first_name="John", last_name="Smith", specialty="Cardiology", practice_name="Heart Health Center", business_phone="555-0123", business_address="123 Medical Plaza, Suite 100" ) refer = ReferCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", diagnosis_codes=["E119"], priority=ReferCommand.Priority.ROUTINE, clinical_question=ReferCommand.ClinicalQuestion.DIAGNOSTIC_UNCERTAINTY, notes_to_specialist="Patient needs cardiac evaluation", service_provider=service_provider ) ``` ### CodeSystems `CodeSystems` provides standardized medical coding system identifiers used throughout Canvas for consistent medical code classification. **Available Code Systems** : Code System| System URI| Description ---|---|--- `SNOMED`| `http://snomed.info/sct`| Systematized Nomenclature of Medicine Clinical Terms `RXNORM`| `http://www.nlm.nih.gov/research/umls/rxnorm`| RxNorm — standardized nomenclature for medications `LOINC`| `http://loinc.org`| Logical Observation Identifiers Names and Codes (labs/observations) `FDB`| `http://www.fdbhealth.com/`| First Databank drug knowledge base `ICD10`| `ICD-10`| International Classification of Diseases, 10th Revision `CVX`| `http://hl7.org/fhir/sid/cvx`| CDC codes for administered vaccines `CPT`| `http://www.ama-assn.org/go/cpt`| Current Procedural Terminology (AMA procedure codes) `NUCC`| `http://www.nucc.org/`| National Uniform Claim Committee provider taxonomy codes `NDC`| `http://hl7.org/fhir/sid/ndc`| National Drug Code `HCPCS`| `http://www.cms.gov/medicare/coding/medhcpcsgeninfo`| Healthcare Common Procedure Coding System `UNITS_OF_MEASURE`| `http://unitsofmeasure.org`| Unified Code for Units of Measure (UCUM) `FULLSCRIPT`| `http://fullscript.com`| Fullscript supplement/dispensary code system `UNSTRUCTURED`| `UNSTRUCTURED`| Canvas-specific system for unstructured or custom codes **Usage Example** : ```python from canvas_sdk.commands.constants import CodeSystems, Coding # Using different code systems icd10_coding = Coding( system=CodeSystems.ICD10, code="E11.9", display="Type 2 diabetes mellitus without complications" ) snomed_coding = Coding( system=CodeSystems.SNOMED, code="65921008", display="Drink plenty of fluids" ) unstructured_coding = Coding( system=CodeSystems.UNSTRUCTURED, code="Custom instruction text" ) ``` ### Coding `Coding` represents a coded value from a medical terminology system, providing structured representation of medical concepts. Field Name| Type| Description ---|---|--- `system`| _string_| The coding system identifier (e.g., ICD-10, SNOMED) `code`| _string_| The specific code within the system `display`| _Optional[string]_| Human-readable description of the code **Usage Example** : ```python from canvas_sdk.commands import InstructCommand from canvas_sdk.commands.constants import CodeSystems, Coding # Using structured coding with SNOMED instruct_snomed = InstructCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", coding=Coding( system=CodeSystems.SNOMED, code="65921008", display="Drink plenty of fluids" ), comment="To address mild dehydration symptoms" ) # Using unstructured coding for custom instructions instruct_custom = InstructCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", coding=Coding( system=CodeSystems.UNSTRUCTURED, code="Physical medicine neuromuscular training" ) ) ``` ### ReportReviewCommunicationMethod The `communication_method` value shared by the review commands — ImagingReview, LabReview, ReferralReview, and UncategorizedDocumentReview. ```python from canvas_sdk.commands.commands.review import ReportReviewCommunicationMethod ``` Communication Method| Value| Description ---|---|--- `DELEGATED_CALL_CAN_LEAVE_MESSAGE`| `"DM"`| Delegated call - can leave message `DELEGATED_CALL_NEED_ANSWER`| `"DA"`| Delegated call - need answer `DELEGATED_LETTER`| `"DL"`| Delegated letter to be sent to patient `ALREADY_LEFT_MESSAGE`| `"AM"`| Already left message for patient `ALREADY_REVIEWED_WITH_PATIENT`| `"AR"`| Already reviewed with patient --- # Provider Companion Source: https://docs.canvasmedical.com/sdk/companion/ The **Provider Companion** is a mobile-optimized, provider-facing web app that runs alongside Canvas. It's designed for the phone-in-hand moments in a clinician's day — looking something up between rooms, triaging a message list, checking in on a patient's tasks — rather than the sit-down charting workflows the desktop app covers. Your plugins can contribute applications to the companion just like they do to the desktop. This page covers how plugins integrate, the three companion-specific application scopes, and how to share code when you want the same plugin to work across multiple scopes. ## Accessing the companion The companion lives at `/companion/` on your Canvas instance: ```text https://.canvasmedical.com/companion/ ``` Any staff user who can log in to the desktop Canvas app can log in to the companion with the same credentials. Patients don't have access — it's a provider surface only. ## How plugins extend the companion Canvas plugins contribute embedded apps through the `Application` handler — a Python class with an `on_open()` method that returns a URL for Canvas to iframe into its UI. Which surface your app appears on is controlled by the `scope` value in your plugin's [`CANVAS_MANIFEST.json`](/sdk/canvas_manifest/#applications). Companion apps work exactly the same way; they just use one of three companion-specific `scope` values. If you haven't built an embedded app before, start with the [Applications](/sdk/handlers-applications/) page — this page assumes you know the basics and focuses on what's companion-specific. There are three companion scopes: Scope| Where the app shows up ---|--- `provider_companion_global`| Icon in the app launcher on the companion's main page, outside of any patient context `provider_companion_patient_specific`| Tab on the patient page, next to the built-in Timeline tab `provider_companion_note_specific`| Tab within an opened note on the patient page When a user opens your app, Canvas fires an `APPLICATION__ON_OPEN` event and your handler's `on_open()` runs. Return a `LaunchModalEffect` pointing at whatever URL you want embedded — typically a page served by a SimpleAPI handler in the same plugin. ## `provider_companion_global` Global-scope apps appear in the companion's launcher on the main page. They run with no patient or note context — they're the right surface for workflows that span many patients, or administrative work that isn't tied to a chart. ![](/assets/images/sdk/companion/companion-global.png) ![](/assets/images/sdk/companion/global-app.png) ### Event context `self.event.context` contains the acting user but no patient or note keys. ### Use cases - A **task queue** showing every task assigned to the logged-in provider across all patients. - A **schedule viewer** for the day's appointments. - A **secure chat** for the care team. - An **administrative dashboard** — e.g. open lab orders awaiting review. ### Example ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.launch_modal import LaunchModalEffect from canvas_sdk.handlers.application import Application class TaskDashboardGlobal(Application): """Companion global app — every task for the logged-in provider.""" def on_open(self) -> Effect: return LaunchModalEffect( url="/plugin-io/api/task_dashboard/app/tasks", target=LaunchModalEffect.TargetType.DEFAULT_MODAL, ).apply() ``` `CANVAS_MANIFEST.json`: ```json { "sdk_version": "0.1.4", "plugin_version": "0.0.1", "name": "task_dashboard", "description": "A task dashboard for the provider companion.", "components": { "applications": [ { "class": "task_dashboard.applications.global_app:TaskDashboardGlobal", "name": "Tasks", "description": "All tasks assigned to me.", "scope": "provider_companion_global", "icon": "assets/tasks.png" } ], "handlers": [ { "class": "task_dashboard.handlers.api:TaskDashboardAPI", "description": "Serves the task dashboard page the iframe loads." } ], "commands": [], "content": [], "effects": [], "views": [] }, "variables": [], "tags": {}, "references": [], "license": "", "diagram": false, "readme": "./README.md" } ``` The `TaskDashboardAPI` class is the [SimpleAPI handler](/sdk/handlers-simple-api/) that actually serves the page `on_open` iframes. `on_open` only returns the launch effect; the page itself has to be served by something, and that something is a SimpleAPI handler registered here. ## `provider_companion_patient_specific` Patient-scope apps appear as tabs on the patient page. Your tab sits next to the built-in Timeline tab, and when the user taps it, your handler's `on_open()` fires with the patient in `event.context`. ![](/assets/images/sdk/companion/patient-timeline.png) ![](/assets/images/sdk/companion/patient-app.png) ### Event context ```python self.event.context["patient"]["id"] # Patient id (UUID string) ``` ### Use cases - A **chart summary** of conditions, meds, allergies, vitals, etc. for the open patient. - A **risk-score panel** tied to the patient. - A **care-plan checklist** for this patient's current programs. - A **patient-scoped task list** — the same tasks view as the global app, but filtered to just this patient's tasks. ### Example ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.launch_modal import LaunchModalEffect from canvas_sdk.handlers.application import Application class TaskDashboardPatient(Application): """Companion patient app — tasks filtered to one patient.""" def on_open(self) -> Effect: patient_id = self.event.context.get("patient", {}).get("id", "") return LaunchModalEffect( url=f"/plugin-io/api/task_dashboard/app/tasks?patient_id={patient_id}", target=LaunchModalEffect.TargetType.DEFAULT_MODAL, ).apply() ``` `CANVAS_MANIFEST.json`: ```json { "sdk_version": "0.1.4", "plugin_version": "0.0.1", "name": "task_dashboard", "description": "A patient-scoped task dashboard for the provider companion.", "components": { "applications": [ { "class": "task_dashboard.applications.patient_app:TaskDashboardPatient", "name": "Tasks", "description": "Tasks for this patient.", "scope": "provider_companion_patient_specific", "icon": "assets/tasks.png" } ], "handlers": [ { "class": "task_dashboard.handlers.api:TaskDashboardAPI", "description": "Serves the task dashboard page the iframe loads." } ], "commands": [], "content": [], "effects": [], "views": [] }, "variables": [], "tags": {}, "references": [], "license": "", "diagram": false, "readme": "./README.md" } ``` ## `provider_companion_note_specific` Note-scope apps appear as tabs inside an opened note on the patient page. Use these for workflows scoped to a single encounter. Both the patient and the note are passed in the event context. ![](/assets/images/sdk/companion/note.png) ![](/assets/images/sdk/companion/note-app.png) ### Event context ```python self.event.context["patient"]["id"] # Patient id (UUID string) self.event.context["note"]["id"] # Note UUID ``` ### Use cases - A **documentation assistant** that reads the note's commands and suggests improvements. - A **coding / billing helper** that computes E&M level from the note's content. - A **visit-specific questionnaire** the provider fills out per encounter. - An **inline scribe** that writes note content from dictation or an LLM. ### Example ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.launch_modal import LaunchModalEffect from canvas_sdk.handlers.application import Application class ScribeAssistant(Application): """Companion note app — inline scribe for the open note.""" def on_open(self) -> Effect: patient_id = self.event.context.get("patient", {}).get("id", "") note_id = self.event.context.get("note", {}).get("id", "") return LaunchModalEffect( url=( f"/plugin-io/api/scribe/app/compose" f"?patient_id={patient_id}¬e_id={note_id}" ), target=LaunchModalEffect.TargetType.DEFAULT_MODAL, ).apply() ``` `CANVAS_MANIFEST.json`: ```json { "sdk_version": "0.1.4", "plugin_version": "0.0.1", "name": "scribe", "description": "Inline scribe for the provider companion.", "components": { "applications": [ { "class": "scribe.applications.note_app:ScribeAssistant", "name": "Scribe", "description": "Inline scribe for this note.", "scope": "provider_companion_note_specific", "icon": "assets/scribe.png" } ], "handlers": [ { "class": "scribe.handlers.api:ScribeAPI", "description": "Serves the scribe page the iframe loads." } ], "commands": [], "content": [], "effects": [], "views": [] }, "variables": [], "tags": {}, "references": [], "license": "", "diagram": false, "readme": "./README.md" } ``` ### Originating commands on the note The note scope's real payoff is that your app can contribute to the note it's running in. Take the `note_uuid` from the event context, build an SDK command with it, and return the command's `originate()` effect alongside your JSON response — the platform will materialize the command in the note after your handler returns. ```python from http import HTTPStatus from canvas_sdk.commands.commands.vitals import VitalsCommand from canvas_sdk.effects.simple_api import JSONResponse from canvas_sdk.handlers.simple_api import SimpleAPI, StaffSessionAuthMixin, api class VitalsEntryAPI(StaffSessionAuthMixin, SimpleAPI): PREFIX = "/app" @api.post("/vitals") def submit(self): note_id = self.request.query_params.get("note_id", "") body = self.request.json() or {} command = VitalsCommand( note_uuid=note_id, blood_pressure_systole=body.get("bp_sys"), blood_pressure_diastole=body.get("bp_dia"), pulse=body.get("pulse"), ) return [ command.originate(), JSONResponse( {"status": "submitted"}, status_code=HTTPStatus.ACCEPTED, ), ] ``` The Application passes `note_id` through to the iframe on the launch URL's query string; the iframe sends it along on the POST to `/vitals`. The same pattern works for any SDK command that exposes an `originate()` method — assessments, prescriptions, lab orders, imaging orders, etc. **Attribution is the plugin author's responsibility.** Commands don't carry an explicit originator field; the platform attributes them to whoever the authenticated session belongs to when the effect is applied. The [`StaffSessionAuthMixin`](/sdk/handlers-simple-api-http/#staff-session) on the handler above ensures the request is gated on a logged-in staff session, so the originated command is attributed to that staff user rather than a generic plugin service identity. If your handler doesn't enforce a staff session, commands it originates won't be tied to the provider using the app — gate every command-originating route with `StaffSessionAuthMixin` (or a stricter equivalent). ## Sharing code across scopes You don't need a separate plugin per scope — a single plugin can register several applications, all backed by the same SimpleAPI handler and the same UI bundle. The Application subclasses differ only in which scope they declare and what they put in the launch URL's query string; the shared handler branches on what it receives. The task dashboard is a natural example. The global view and the patient-scoped view show the same UI — a list of task cards — but one shows every task assigned to the provider and the other shows only tasks tied to the open patient. They can share everything except the scope declaration and one query-string parameter. ### Manifest — two applications, one handler ```json { "sdk_version": "0.1.4", "plugin_version": "0.0.1", "name": "task_dashboard", "description": "Task dashboard — global and patient-scoped from one codebase.", "components": { "applications": [ { "class": "task_dashboard.applications.global_app:TaskDashboardGlobal", "name": "Tasks", "description": "All tasks assigned to me.", "scope": "provider_companion_global", "icon": "assets/tasks.png" }, { "class": "task_dashboard.applications.patient_app:TaskDashboardPatient", "name": "Tasks", "description": "Tasks for this patient.", "scope": "provider_companion_patient_specific", "icon": "assets/tasks.png" } ], "handlers": [ { "class": "task_dashboard.handlers.api:TaskDashboardAPI", "description": "Serves the task dashboard page and JSON bundle." } ], "commands": [], "content": [], "effects": [], "views": [] }, "variables": [], "tags": {}, "references": [], "license": "", "diagram": false, "readme": "./README.md" } ``` ### Shared SimpleAPI handler The handler serves the same HTML for both scopes and branches on `patient_id` in its data endpoint: present → filter to that patient, absent → return the provider's entire task queue. ```python from http import HTTPStatus from canvas_sdk.effects.simple_api import HTMLResponse, JSONResponse from canvas_sdk.handlers.simple_api import SimpleAPI, StaffSessionAuthMixin, api from canvas_sdk.templates import render_to_string from canvas_sdk.v1.data.task import Task class TaskDashboardAPI(StaffSessionAuthMixin, SimpleAPI): PREFIX = "/app" @api.get("/tasks") def page(self): return [HTMLResponse( render_to_string("static/tasks.html"), status_code=HTTPStatus.OK, )] @api.get("/tasks/data.json") def data(self): user_id = self.request.headers["canvas-logged-in-user-id"] patient_id = self.request.query_params.get("patient_id") tasks = Task.objects.filter(assigned_to_id=user_id) if patient_id: tasks = tasks.filter(patient__key=patient_id) return [JSONResponse( {"tasks": [serialize(t) for t in tasks]}, status_code=HTTPStatus.OK, )] ``` The HTML/JS bundle (served by `/app/tasks`) fetches `/app/tasks/data.json` relative to its own URL, so it gets the right slice of tasks without knowing which scope launched it — the scope is encoded in whether the Application handler appended `?patient_id=...` to the launch URL. This pattern generalizes: any time the _same UI_ works on a filtered or unfiltered dataset, you can register one Application per scope and share the handler, templates, and client-side code underneath. ## Dismissing your modal Shortly after your iframe loads, Canvas transfers a `MessagePort` to it via a `postMessage` event. The plugin stores that port and posts `{type: 'CLOSE_MODAL'}` through it whenever it wants to dismiss itself — typically right after a successful form submit, or from a Cancel button. ```javascript let messagePort = null; window.addEventListener('message', (event) => { if (event.data?.type === 'INIT_CHANNEL' && event.ports?.[0]) { messagePort = event.ports[0]; messagePort.start(); } }); function closeModal() { if (messagePort) { messagePort.postMessage({ type: 'CLOSE_MODAL' }); } else { window.close(); // fallback if the port never arrived } } ``` Register the `message` listener at module scope, not inside a `DOMContentLoaded` handler — the port can arrive before your DOM is ready, and a listener attached later will miss it. ## Async effects from SimpleAPI handlers Effects returned from a SimpleAPI route execute **after** the handler returns — they're dispatched to a platform worker, not applied inside your handler's transaction. That means you can't emit e.g. `Patient(...).create()` and then query for the new patient in the same request — the effect hasn't been processed yet and the record doesn't exist. If you need the new record's UUID (for example to deep-link to it), do the lookup on a follow-up request from the iframe. The simplest shape: `POST /create` emits the effect and returns `202 Accepted` with everything needed to identify the new record; the iframe polls a separate `GET /find` endpoint until the record appears (or a short timeout fires). ```python import datetime from http import HTTPStatus from canvas_sdk.effects.patient import Patient as PatientEffect from canvas_sdk.effects.simple_api import JSONResponse from canvas_sdk.handlers.simple_api import SimpleAPI, StaffSessionAuthMixin, api from canvas_sdk.v1.data.patient import Patient class RegisterPatientAPI(StaffSessionAuthMixin, SimpleAPI): PREFIX = "/app" @api.post("/create") def create(self): body = self.request.json() or {} # ... validate the submission ... effect = PatientEffect( first_name=body["first_name"], last_name=body["last_name"], birthdate=body["birth_date"], ).create() return [ effect, JSONResponse({ "status": "submitted", "lookup_params": { "first_name": body["first_name"], "last_name": body["last_name"], "birth_date": body["birth_date"], }, "lookup_started_at": datetime.datetime.now(datetime.timezone.utc).isoformat(), }, status_code=HTTPStatus.ACCEPTED), ] @api.get("/find") def find(self): params = self.request.query_params found = ( Patient.objects .filter( first_name=params["first_name"], last_name=params["last_name"], birth_date=params["birth_date"], created__gte=params["after"], ) .order_by("-created") .first() ) return [JSONResponse( {"patient_id": str(found.id) if found else None}, status_code=HTTPStatus.OK, )] ``` On the iframe, poll `/find` at ~500 ms intervals for ~5 s. On the first hit, deep-link to the new record. On timeout, surface a clear error message — the effect failed and there's nothing to link to. Don't claim success before the lookup confirms the record exists. ## Common patterns The conventions are the same as any other SDK application — the companion just chooses where and when to render your iframe. - **Serve your UI from a[SimpleAPI handler](/sdk/handlers-simple-api/)** in the same plugin. `on_open()` returns a `LaunchModalEffect` pointing at a URL like `/plugin-io/api//...`. - **Authenticate with[`StaffSessionAuthMixin`](/sdk/handlers-simple-api-http/#staff-session).** The companion is staff-only, so every request hitting your plugin's SimpleAPI should be gated on a valid staff session. Mix the class in instead of writing your own `authenticate()` — it rejects non-staff sessions (including patient-portal sessions) up front: ```python from canvas_sdk.handlers.simple_api import SimpleAPI, StaffSessionAuthMixin class TaskDashboardAPI(StaffSessionAuthMixin, SimpleAPI): PREFIX = "/app" # ... routes ... ``` The logged-in user is then available via `self.request.headers["canvas-logged-in-user-id"]`. - **Push live updates with a plugin-owned WebSocket.** If your app needs to stay in sync when data changes, add a `BaseHandler` that listens for the relevant domain events and broadcasts on a channel the iframe subscribes to. See [WebSocket API](/sdk/handlers-simple-api-websocket/) for the handler shape and authentication flow. - **Keep it mobile-first.** The companion runs on phones. System fonts, stacked sections, generous tap targets, no hover interactions — your app should feel native inside the companion's shell. - **Drop your own top chrome in patient and note scope.** The companion harness already renders the patient's name (and, in note scope, the note type and date) above your iframe. If your plugin also renders a title bar, the result is a doubled-up header. Suppress the iframe's header when running in patient or note scope — either branch in your Application and pass a hint through the launch URL, or scope the CSS on a body class your shell sets from the query string. - **Link out to another patient with`window.top.location`.** To navigate from inside your modal to another patient's companion view — for example, a "tap a patient name to jump there" pattern — set `window.top.location = "/companion/patient//"`. That tears down the iframe and replaces the parent page. Setting only the iframe's `location` leaves the modal open showing the patient page on top of whatever was behind it, which is rarely what you want. ## Further reading - [Applications](/sdk/handlers-applications/) — the base `Application` handler class, `on_open()`, and other application scopes. - [SimpleAPI](/sdk/handlers-simple-api/) — serving HTML and JSON from a plugin. - [WebSocket API](/sdk/handlers-simple-api-websocket/) — pushing live updates to an iframe. - [Data module](/sdk/data/) — read-only clinical data models. - Example plugin: [`example_provider_companion_app`](https://github.com/canvas-medical/canvas-plugins/tree/main/example-plugins/example_provider_companion_app) demonstrates one plugin registering apps at all three companion scopes. --- # AttributeHubs Source: https://docs.canvasmedical.com/sdk/custom-data-attribute-hubs/ ## Overview `AttributeHubs` provide a simple mechanism for storing arbitrary data that doesn't belong to existing models, or does not conform to a traditional database schema. An AttributeHub is merely a collection of named attributes with values. This approach is ideal for cross-cutting concerns that span multiple models, temporary data storage, external system state tracking, data model prototyping, or plugin-specific configuration. **Best for:** - Cross-cutting state that spans multiple models (sync cursors, external IDs) - One-off or small-collection configuration and state - Data with no natural schema (varying fields per record) - External system state tracking **Example use cases:** - API synchronization state - External system identifiers - Plugin configuration and feature flags **Not ideal for** entities with relationships, large collections you need to search or paginate, or data requiring aggregation or reporting. See [Design Considerations](/sdk/custom-data-design-considerations/) for detailed guidance. ## Prerequisites AttributeHub tables live inside your plugin's custom data namespace, so a plugin must declare a `custom_data` block in its [`CANVAS_MANIFEST.json`](/sdk/canvas_manifest/#custom-data) before it can read or write hubs: ```json "custom_data": { "namespace": "my_org__my_plugin", "access": "read_write" } ``` Use `read_write` to create hubs or set attributes. `read` is enough to query hubs in a namespace that another plugin already owns. A plugin that creates its own namespace needs no access key. Canvas generates `namespace_read_access_key` and `namespace_read_write_access_key` when the first `read_write` plugin initializes the namespace, and stores both on that plugin so you can retrieve them from the Admin UI. A plugin joining a namespace another plugin owns declares the matching key as a variable, so that an operator can set the value at install time: ```json "variables": [ {"name": "namespace_read_write_access_key", "sensitive": true} ] ``` Defining a [CustomModel](/sdk/custom-data-custom-models/) is optional. AttributeHubs need only the `custom_data` block, with no `models` directory. See the [Quick Start](/sdk/custom-data-quick-start/) for full namespace setup and [Namespace Lifecycle](/sdk/custom-data-namespace-lifecycle/) for how namespaces are created and shared between plugins. > **Info:** A plugin with no `custom_data` block has no namespace, and every AttributeHub query it makes fails with `psycopg.errors.UndefinedTable: relation "attribute_hub" does not exist`. ## Creating an AttributeHub Create a hub for a specific purpose using the `type` and `id` fields, which together form a unique key. There is a database constraint on these two fields to ensure uniqueness, and creating a duplicate will raise a `UniqueViolation` exception. ```python from canvas_sdk.v1.data import AttributeHub # Create a hub for a specific purpose hub = AttributeHub.objects.create( type="staff_profile", id="staff_id:abc123" ) ``` ## Storing Data in AttributeHub Store individual attributes or complex data as JSON. Here's an example of a meal tracker that records patient meals and calories: ```python from datetime import datetime from canvas_sdk.v1.data import AttributeHub, Patient patient = Patient.objects.get(id="patient-uuid-here") # Create a hub to track a specific meal hub = AttributeHub.objects.create( type="meal_entry", id=f"patient:{patient.id}:meal:{datetime.now().isoformat()}" ) # Store individual attributes hub.set_attribute("meal_type", "lunch") hub.set_attribute("calories", 650) hub.set_attribute("recorded_at", datetime.now()) # Store complex data as JSON meal_details = { "foods": [ {"name": "Grilled chicken salad", "calories": 350, "protein_g": 35}, {"name": "Whole grain roll", "calories": 150, "protein_g": 5}, {"name": "Apple", "calories": 95, "protein_g": 0}, {"name": "Water", "calories": 0, "protein_g": 0} ], "notes": "Patient reported feeling satisfied after meal" } hub.set_attribute("meal_details", meal_details) # Store multiple attributes at once hub.set_attributes({ "total_protein_g": 40, "meal_location": "home", "logged_by": "patient_self_report" }) ``` ## Retrieving Data from AttributeHub Use the get-or-create pattern to retrieve existing hubs or create new ones: ```python from canvas_sdk.v1.data import AttributeHub, Patient patient = Patient.objects.get(id="patient-uuid-here") # Get or create a hub for tracking daily calorie totals hub, created = AttributeHub.objects.get_or_create( type="daily_calorie_summary", id=f"patient:{patient.id}:date:2024-01-15" ) if created: # Initialize a new day's tracking hub.set_attributes({ "total_calories": 0, "meal_count": 0, "calorie_goal": 2000 }) # Retrieve attributes total_calories = hub.get_attribute("total_calories") meal_count = hub.get_attribute("meal_count") calorie_goal = hub.get_attribute("calorie_goal") # Handle missing attributes gracefully notes = hub.get_attribute("daily_notes") # Returns None if not set ``` ## Supported Value Types Attributes are automatically stored in appropriately typed database columns. The column is selected based on the Python type of the value you pass to `set_attribute()`: ```python from datetime import date, datetime from canvas_sdk.v1.data import AttributeHub hub = AttributeHub.objects.get(type="staff_profile", id="staff_id:abc123") # String values hub.set_attribute("bio", "Board-certified cardiologist") # Integer values hub.set_attribute("patient_capacity", 100) # Boolean values hub.set_attribute("accepting_patients", True) # Decimal values hub.set_attribute("rating", 4.8) # Date values hub.set_attribute("creation_date", date.today()) # Datetime values hub.set_attribute("last_updated", datetime.now()) # JSON/Complex objects (dicts, lists) hub.set_attribute("preferences", { "notification_email": True, "notification_sms": False }) ``` Each attribute is a `CustomAttribute` row, and the column a value lands in is chosen from its Python type: Field Name| Python Type| Django Field Type| PostgreSQL Data Type ---|---|---|--- `text_value`| `str`| `TextField`| `text` `int_value`| `int`| `IntegerField`| `integer` `bool_value`| `bool`| `BooleanField`| `boolean` `decimal_value`| `float`, `Decimal`| `DecimalField`| `decimal(20,10)` `date_value`| `date`| `DateField`| `date` `timestamp_value`| `datetime`| `DateTimeField`| `timestamp with time zone` `json_value`| `dict`, `list`| `JSONField`| `jsonb` These typed columns can be referenced directly in queries. See When to Use Explicit Field Names for cases where you need to target a specific column. ## Querying AttributeHubs by Attribute Values Find AttributeHubs based on the values stored in their attributes using `custom_attributes__value`. The SDK automatically routes the filter to the correct typed column based on the Python type of the value you pass in: ```python from canvas_sdk.v1.data import AttributeHub # Find hubs with a specific string attribute lunch_hubs = AttributeHub.objects.filter( type="meal_entry", custom_attributes__name="meal_type", custom_attributes__value="lunch", ) # Find hubs with a calorie count above a threshold high_calorie = AttributeHub.objects.filter( type="meal_entry", custom_attributes__name="calories", custom_attributes__value__gte=500, ) # Find hubs with a boolean flag active_flags = AttributeHub.objects.filter( type="feature_flags", custom_attributes__name="enabled", custom_attributes__value=True, ) ``` You can also filter attribute objects directly, for example when working with a hub's related attributes: ```python from canvas_sdk.v1.data import AttributeHub hub = AttributeHub.objects.get(type="meal_entry", id="patient:abc:meal:2024-01-15T12:00") # Filter the hub's own attributes high_cal_attrs = hub.custom_attributes.filter(value__gte=500) ``` ### When to Use Explicit Field Names In most cases `custom_attributes__value` (or `value` on a hub's related attributes) is sufficient. However, you must reference the typed column directly in the following cases: - **JSON containment queries.** PostgreSQL's `@>` containment operator on `jsonb` has different semantics from the `LIKE '%...%'` that `__contains` produces on a text column. Since `value__contains` with a string argument targets `text_value`, you must use `json_value__contains` to perform JSON containment checks: ```python from django.db.models import Q from canvas_sdk.v1.data import AttributeHub # Find hubs whose "specialties" JSON array contains "Cardiology" AttributeHub.objects.filter( type="staff_profile", custom_attributes__name="specialties", custom_attributes__json_value__contains="Cardiology", ) # OR across multiple JSON values specialty_filters = Q() for specialty in ["Cardiology", "Internal Medicine"]: specialty_filters |= Q(custom_attributes__json_value__contains=specialty) AttributeHub.objects.filter( Q(custom_attributes__name="specialties") & specialty_filters ) ``` - **Custom JSON lookups.** Django's `JSONField` supports lookups like `__has_key`, `__contained_by`, and key-path access (`json_value__key__nested`). These are only available on the `json_value` column directly. - **Ambiguous Python types.** The `value` rewriter uses `type()` (not `isinstance()`) to select the column. If you pass a string but intend to query `json_value` (or vice versa), the rewriter will target the wrong column. Use the explicit field name when the Python type of your filter value doesn't match the storage column. - **Null checks across relations.** `custom_attributes__value=None` and `custom_attributes__value__isnull` are not supported on `AttributeHub.objects.filter(...)` and will raise `TypeError`. Null checks require testing every typed column, which produces unreliable results when combined with Django's cross-relation JOIN machinery. Use explicit column names instead: ```python from canvas_sdk.v1.data import AttributeHub # Check whether a specific column is null across the relation AttributeHub.objects.filter( type="staff_profile", custom_attributes__name="specialty", custom_attributes__text_value__isnull=True, ) ``` Note that `value=None` and `value__isnull` _are_ supported for direct queries on a hub's own attributes (e.g., `hub.custom_attributes.filter(value__isnull=True)`), where no cross-relation join is involved. Refer to Supported Value Types for the mapping between Python types and database columns. ## Optimizing Queries with Prefetch By default, the AttributeHub manager prefetches all custom attributes when you query hubs. This means accessing `hub.get_attribute(...)` after a query does not trigger additional database queries: ```python from canvas_sdk.v1.data import AttributeHub # All custom attributes are prefetched automatically hubs = AttributeHub.objects.filter(type="meal_entry") for hub in hubs: # No additional queries — attributes are already loaded meal_type = hub.get_attribute("meal_type") calories = hub.get_attribute("calories") ``` ### Prefetching Specific Attributes When a hub has many attributes but you only need a few, use `with_only()` to prefetch only the attributes you need. This reduces the amount of data transferred from the database: ```python from canvas_sdk.v1.data import AttributeHub # Prefetch only the "calories" and "meal_type" attributes hubs = AttributeHub.objects.with_only(["calories", "meal_type"]).filter(type="meal_entry") for hub in hubs: calories = hub.get_attribute("calories") # Loaded from prefetch cache meal_type = hub.get_attribute("meal_type") # Loaded from prefetch cache notes = hub.get_attribute("notes") # Falls back to a DB query (not prefetched) # Prefetch a single attribute hub = AttributeHub.objects.with_only("campaign_status").get( type="crm_sync", id="patient:abc123" ) ``` If you access an attribute that was not included in `with_only()`, it will fall back to a database query. Use `with_only()` as an optimization, not a filter. ## Use Case Example: CRM Campaign Sync Store synchronization state between a custom data model and an external CRM using AttributeHub: ```python from canvas_sdk.handlers.simple_api import SimpleAPI, api from canvas_sdk.effects.simple_api import JSONResponse from canvas_sdk.v1.data import AttributeHub, Patient from datetime import datetime class CRMSyncAPI(SimpleAPI): """API endpoint for syncing campaign data with external CRM.""" PREFIX = "/crm" @api.post("/campaign//patient/") def sync_patient_campaign(self): campaign_id = self.request.path_params["campaign_id"] patient_id = self.request.path_params["patient_id"] patient = Patient.objects.get(id=patient_id) crm_data = self.request.json() # Store CRM sync state in AttributeHub hub, created = AttributeHub.objects.get_or_create( type="crm_campaign_sync", id=f"patient:{patient.id}:campaign:{campaign_id}" ) hub.set_attributes({ "crm_contact_id": crm_data.get("contact_id"), "campaign_status": crm_data.get("status"), "enrollment_date": crm_data.get("enrolled_at"), "last_synced": datetime.now(), "sync_direction": "crm_to_canvas" }) return [JSONResponse({"status": "success", "hub_id": str(hub.id)})] ``` Later, retrieve the sync state when processing patient events: ```python from canvas_sdk.handlers.base import BaseHandler from canvas_sdk.events import EventType from canvas_sdk.v1.data import AttributeHub class CampaignEnrollmentHandler(BaseHandler): """Handler that checks CRM campaign sync state for patients.""" RESPONDS_TO = EventType.Name(EventType.PATIENT_UPDATED) def compute(self): patient_id = self.target.id campaign_id = "wellness_2024" # Your campaign identifier # Retrieve CRM sync state from AttributeHub hub, created = AttributeHub.objects.get_or_create( type="crm_campaign_sync", id=f"patient:{patient_id}:campaign:{campaign_id}" ) if not created: crm_contact_id = hub.get_attribute("crm_contact_id") campaign_status = hub.get_attribute("campaign_status") last_synced = hub.get_attribute("last_synced") # Use the CRM data to drive clinical workflows if campaign_status == "enrolled": # Patient is enrolled in CRM campaign - trigger relevant protocols pass return [] ``` ## Best Practices ### Data Organization 1. **Use descriptive type values** \- Choose meaningful type names that describe the purpose of the hub (e.g., "external_sync", "api_cache", "feature_flags") 2. **Use consistent ID patterns** \- Use a consistent pattern for `id` (e.g., "entity_type:entity_id") 3. **Namespace by purpose** \- Group related data under a single hub rather than creating multiple hubs for the same entity type ### Data Privacy and Isolation 1. **Understand plugin data scoping** \- All AttributeHub data is isolated to your plugin's namespace 2. **Implement proper authorization** \- Secure all APIs that expose AttributeHub data 3. **Follow PHI guidelines** \- Treat all patient-related data with appropriate security measures ### Performance 1. **Batch attribute updates** \- Use `set_attributes()` to set multiple values at once 2. **Cache hub lookups** \- If accessing the same hub multiple times, store the reference ### Data Integrity 1. **Use get_or_create** \- Use `get_or_create()` to avoid duplicate hubs 2. **Handle None values** \- Always check if an attribute exists before using it 3. **Validate data** \- Validate data before storing in AttributeHub 4. **Clean up unused data** \- Remove AttributeHub instances that are no longer needed ### Testing 1. **Use get_or_create in tests** \- This pattern works well for test isolation 2. **Isolate test data** \- Create all data required by the test, within the test ## See Also - [Custom Data Overview](/sdk/custom-data/) \- Overview of all custom data techniques - [Design Considerations](/sdk/custom-data-design-considerations/) \- Choosing the right technique and avoiding anti-patterns - [CustomModels](/sdk/custom-data-custom-models/) \- Structured models with relationships - [Sharing Data](/sdk/custom-data-sharing-data/) \- Sharing data among plugins - [Testing Custom Data](/sdk/custom-data-testing/) \- Testing utilities and examples - [Data Models](/sdk/data/) \- Core SDK data models - [Caching API](/sdk/caching) \- Auto-expiring transient data - [Secrets](/sdk/secrets/) \- Managing API keys and sensitive configuration --- # CustomModels Source: https://docs.canvasmedical.com/sdk/custom-data-custom-models/ ## Overview CustomModels allow you to define fully structured, typed data models with relationships among entities and normalized data. Built on Django's ORM, CustomModels provide the most powerful and flexible approach to storing custom data in Canvas plugins. The functionality expressed is a subset of the total ORM. The SDK omits some features in order to simplify the lifecycle of plugin installation and maintenance. **Best for:** - Structured data with a stable, known schema - Relationships between entities (foreign keys, join tables) - Data requiring compound filtering, sorting, or aggregation - Data consumed by reports or analytics **Example use cases:** - Provider specialties and certifications - Constructing new associations among Canvas SDK models - Custom workflows and forms - Integration-specific data structures - Practice-specific business entities **Not ideal for** simple metadata on existing models, highly variable or schemaless data, or ephemeral data. Tables and columns cannot be dropped once created. See [Design Considerations](/sdk/custom-data-design-considerations/) for detailed guidance. Custom models may be associated to core SDK data models by extending them with `ModelExtension`, or may be entirely standalone. As an example, a `StaffBiography` CustomModel could attach to a `CustomStaff(Staff, ModelExtension)` class, and be accessible via a `biography` property on `CustomStaff`. Custom models must be defined within a `models` directory under the plugin top-level directory. E.g., `/my_plugin/models/custom_model_a.py`. If not, then database migrations will not be applied. (Extended SDK models may be defined anywhere since they do not require any database modifications.) * * * ## Basic CustomModel Create a custom model by extending `CustomModel`: ```python from canvas_sdk.v1.data.base import CustomModel from django.db.models import BooleanField, DateField, DateTimeField, DecimalField, IntegerField, JSONField, TextField class HealthCoach(CustomModel): name = TextField() practicing_since = IntegerField() version = DecimalField(default=1.0, decimal_places=1, max_digits=3) is_accepting_patients = BooleanField() created_date = DateField(auto_now_add=True) last_modified_at = DateTimeField(auto_now_add=True) extended_attributes = JSONField() ``` This above definition will result in a PostgreSQL table named `healthcoach`. It will have a primary key column named `dbid` of type `serial`, an auto-incrementing integer. It will have six additional columns of `text`, `integer`, `numeric(3,8)`, `boolean`, `jsonb`,`date`, and `timestamp with time zone`, respectively. * * * ## Schema Rules and Constraints To maintain safety on potentially large datasets, most constraints on CustomModels are not enforced within the database and must be enforced within plugin code. Unsupported constraints: - `not null` - `max_length` - `references` (database-level foreign key constraints) If applied to an existing dataset, these constraints could result in a full table rewrite operation, or prevent plugin installation. Note that while database-level `REFERENCES` constraints are not created, Django's ORM enforces `on_delete` behavior (`CASCADE`, `SET_NULL`, `DO_NOTHING`) at the application level — see Delete Behavior below. Uniqueness constraints **are** supported via `UniqueConstraint` in `Meta.constraints`. See Uniqueness Constraints below. ### Field Types The Canvas SDK provides Django-based field types for defining your models: Field Type| Description| Supported Parameters ---|---|--- `TextField`| Variable-length text| `default` `IntegerField`| Integer values| `default` `DecimalField`| Decimal numbers| `default`, `max_digits`,`decimal_places` `BooleanField`| True/False values| `default` `DateField`| Date values| `auto_now`, `auto_now_add`, `default` `DateTimeField`| Date and time values| `auto_now`, `auto_now_add`, `default` `JSONField`| JSON-serializable data| `default` `ForeignKey`| Many-to-one relationship| `related_name`, `on_delete`, `to_field` `OneToOneField`| One-to-one relationship| `related_name`, `on_delete`, `to_field`, `primary_key` `ManyToManyField`| Many-to-many relationship| `through` (required), `related_name` If `default` is supplied it will be applied by the Django ORM, and will not be a PostgreSQL default. As a result, only new records will receive the value, and it will not cause a mass edit of existing records. The `on_delete` parameter is required on `ForeignKey` and `OneToOneField`. It controls what happens to child records when the referenced parent record is deleted: Value| Behavior ---|--- `CASCADE`| Automatically delete the child record when the parent is deleted. `SET_NULL`| Set the foreign key column to `NULL` when the parent is deleted. The child record is kept. `DO_NOTHING`| Take no action. The plugin is responsible for cleaning up or preventing orphaned references. These behaviors are enforced by Django's ORM at the application level. They apply when deleting via `model.delete()` or `queryset.delete()`, but not when using raw SQL. ### Indexes Add indexes for frequently queried fields: ```python from canvas_sdk.v1.data.base import CustomModel from django.contrib.postgres.indexes import GinIndex from django.db.models import BooleanField, DateTimeField, Index, IntegerField, JSONField, TextField class ProviderQualification(CustomModel): first_name = TextField() last_name = TextField() board_certified = BooleanField() practicing_since_year = IntegerField() extended_attributes = JSONField() created_at = DateTimeField() class Meta: indexes = [ # Single-column index Index(fields=["practicing_since_year"]), # Composite index for common search combinations Index(fields=["first_name", "last_name"]), # Descending index for ordering records Index(fields=["-created_at"]), # Gin index for efficient JSON queries GinIndex(fields=["extended_attributes"]) ] ``` **Index Best Practices:** - Index fields used in `filter()` and `order_by()` - Create composite indexes for common multi-field queries - **Do not** index `ForeignKey` or `OneToOneField` columns — they are indexed automatically. The SDK will raise an error if you declare a single-column index that duplicates an auto-indexed column. ### Uniqueness Constraints Use `UniqueConstraint` in `Meta.constraints` to enforce uniqueness on one or more columns. Uniqueness is enforced at the database level via a `CREATE UNIQUE INDEX`. ```python from canvas_sdk.v1.data.base import CustomModel from django.db.models import TextField, UniqueConstraint class Specialty(CustomModel): name = TextField() code = TextField() class Meta: constraints = [ UniqueConstraint(fields=["code"], name="uq_specialty_code"), ] ``` Composite uniqueness (multiple columns together must be unique): ```python from canvas_sdk.v1.data.base import CustomModel from django.db.models import DO_NOTHING, ForeignKey, TextField, UniqueConstraint from canvas_sdk.v1.data import Staff, ModelExtension class CustomStaff(Staff, ModelExtension): pass class StaffCertification(CustomModel): staff = ForeignKey(CustomStaff, to_field="dbid", on_delete=DO_NOTHING, related_name="%(app_label)s__certifications") certification_code = TextField() class Meta: constraints = [ UniqueConstraint(fields=["staff", "certification_code"], name="uq_staff_cert"), ] ``` Each `UniqueConstraint` requires a `name` parameter — this is a standard Django requirement. Choose a descriptive name that won't collide with other constraints in your plugin. Use Django field names (e.g., `"staff"`) rather than database column names (e.g., `"staff_id"`) in the `fields` list — the SDK resolves field names to column names automatically. The same applies to `Index` fields in `Meta.indexes`. **Important:** Do not use `unique=True` on individual fields. The SDK will reject it with an error directing you to use `UniqueConstraint` instead. This is because `unique=True` modifies the column definition itself, and our DDL pipeline cannot retroactively alter existing columns — meaning a `unique=True` added after the initial deployment would silently have no effect. **Constraint placement:** `UniqueConstraint` must be placed in `Meta.constraints`, not `Meta.indexes`. Although they are structurally similar to indexes, placing a `UniqueConstraint` in `Meta.indexes` would create a non-unique index. The SDK validates this and raises an error if it detects the mistake. **Lifecycle:** Unique indexes are created with `CREATE UNIQUE INDEX IF NOT EXISTS`, making them safe to add at any time — they are applied idempotently on every deployment. However, if the table already contains duplicate values for the constrained columns, the index creation will fail. Clean up duplicates before adding the constraint. Operation| Allowed| Explanation ---|---|--- Add UniqueConstraint| Yes| A unique index will be created if it does not already exist. Remove UniqueConstraint| No| Remove the constraint from your model and it will be ignored, but the index will remain in the database. * * * ## Creating and Querying ### Creating Records ```python from my_plugin.models import ProviderQualification # Create and save qualification = ProviderQualification( first_name="Jessica", last_name="Smith", board_certified=True, practicing_since_year=2005, extended_attributes={ "biography": "Lives in Fresno with her..." } ) qualification.save() # Create in one step qualification = ProviderQualification.objects.create( first_name="Jessica", last_name="Smith", board_certified=True, practicing_since_year=2005, extended_attributes={ "biography": "Lives in Fresno with her..." } ) # Get or create (avoids duplicates) qualification = ProviderQualification.objects.get_or_create( first_name="Jessica", last_name="Smith", defaults={ "board_certified": True, "practicing_since_year": 2005, "extended_attributes": { "biography": "Lives in Fresno with her..." } } ) ``` ### Querying Records ```python from my_plugin.models import ProviderQualification from datetime import date # Get all records all_qualifications = ProviderQualification.objects.all() # Filter records board_certified = ProviderQualification.objects.filter(board_certified=True) # Get providers with 10+ years experience experienced = ProviderQualification.objects.filter( practicing_since_year__lte=date.today().year - 10 ) # Get single record by database primary key try: jessica = ProviderQualification.objects.get(dbid=123) except ProviderQualification.DoesNotExist: jessica = None # Get single record by fields try: jessica = ProviderQualification.objects.get(first_name="Jessica", last_name="Smith") except ProviderQualification.DoesNotExist: jessica = None # Apply multiple filters senior_certified = ProviderQualification.objects.filter( board_certified=True, practicing_since_year__lte=2010 # Practicing since 2010 or earlier ) # Order results by_experience = ProviderQualification.objects.order_by("practicing_since_year") # Limit results - get 5 most experienced (earliest practicing_since_year) top_five = ProviderQualification.objects.order_by("practicing_since_year")[:5] ``` ### Updating Records ```python from my_plugin.models import ProviderQualification # Update single record qualification = ProviderQualification.objects.get(first_name="Jessica", last_name="Smith") qualification.practicing_since_year = 2004 qualification.save() # Update multiple records ProviderQualification.objects.filter( board_certified=False ).update(board_certified=True) # Update or create qualification, created = ProviderQualification.objects.update_or_create( first_name="Michael", last_name="Johnson", defaults={ "board_certified": True, "practicing_since_year": 2015, "extended_attributes": { "specialties": ["Cardiology", "Internal Medicine"] } } ) ``` ### Deleting Records ```python from my_plugin.models import ProviderQualification # Delete single record qualification = ProviderQualification.objects.get(first_name="Jessica", last_name="Smith") qualification.delete() # Delete multiple records - remove providers who started this year from datetime import date ProviderQualification.objects.filter( practicing_since_year=date.today().year ).delete() # Delete all records (use with caution!) ProviderQualification.objects.all().delete() ``` ## Extending the Canvas Data Model CustomModels may reference core SDK models by creating a proxy model with `ModelExtension`. This gives each plugin its own private handle on a shared SDK model, keeping `related_name` attributes isolated across plugins. ```python from canvas_sdk.v1.data import Staff, ModelExtension class CustomStaff(Staff, ModelExtension): pass ``` No new table is created — `CustomStaff` shares the `Staff` table and behaves identically for queries. Point your `ForeignKey` or `OneToOneField` at the proxy to get clean, un-namespaced reverse lookups. For a full explanation of why proxy models exist, how `related_name` namespacing works, and how to reference SDK models directly without a proxy, see [Extending SDK Models](/sdk/custom-data-extending-sdk-models/). * * * ## One-to-One Relationships A one-to-one relationship links one record in a model to exactly one record in another model. Use `OneToOneField` to define this relationship. ### Basic One-to-One ```python from canvas_sdk.v1.data import Staff, ModelExtension from canvas_sdk.v1.data.base import CustomModel from django.db.models import CASCADE, DateTimeField, DecimalField, OneToOneField, TextField class CustomStaff(Staff, ModelExtension): """Extends Staff with custom attribute support.""" pass class Biography(CustomModel): biography = TextField() language = TextField() version = DecimalField(default=1.0, decimal_places=1, max_digits=3) last_modified_at = DateTimeField(auto_now_add=True) staff = OneToOneField( CustomStaff, to_field="dbid", on_delete=CASCADE, related_name="biography" ) ``` The above will create a table with a `serial` primary key, two `text` columns, a `numeric(1,3)` column, a `timestamptz` column, and an `integer` column named `staff_id` that contains a foreign key into the SDK `Staff` model. The `CustomStaff` class will contain the reverse mapping via `related_name`. **Uniqueness:** A `OneToOneField` implies that the foreign key column is unique — each target record can be referenced by at most one row. The SDK automatically creates a `UNIQUE INDEX` on the foreign key column to enforce this at the database level. Do not add a separate `UniqueConstraint` for it — the SDK will raise an error if you declare a single-column `UniqueConstraint` or `Index` on an auto-indexed column. ### One-to-One with `primary_key=True` A `OneToOneField` can serve as the table's primary key by setting `primary_key=True`. This replaces the default auto-incrementing `dbid` column — the foreign key column becomes the sole primary key. This pattern is useful when the child record has a strict 1:1 relationship with its parent and there is no need for a separate surrogate key. ```python from canvas_sdk.v1.data import Patient, ModelExtension from canvas_sdk.v1.data.base import CustomModel from django.db.models import CASCADE, JSONField, OneToOneField class CustomPatient(Patient, ModelExtension): pass class PatientPreferences(CustomModel): patient = OneToOneField( CustomPatient, to_field="dbid", on_delete=CASCADE, related_name="preferences", primary_key=True ) preferences = JSONField(default=dict) ``` The above will create a table with a single `integer` primary key column `patient_id` (no `dbid` column) and a `jsonb` column. The primary key inherently enforces uniqueness, so no additional unique index is created. **Note:** `primary_key=True` is only supported on `OneToOneField`. Setting it on a `ForeignKey` or any other field type will raise an error — use a `OneToOneField` instead when you need a shared primary key. ### Creating One-to-One Records ```python from my_plugin.models import CustomStaff, Biography # Get the staff member staff = CustomStaff.objects.get(id="staff-uuid") # Create biography biography = Biography.objects.create( staff=staff, biography="Dr. Smith is a board-certified cardiologist with over 20 years of experience...", language="English", version=1.0 ) ``` ### Querying One-to-One Relationships ```python from my_plugin.models import CustomStaff, Biography # Access from biography to staff biography = Biography.objects.get(dbid=1) staff_member = biography.staff # Access from staff to biography (using related_name) staff = CustomStaff.objects.get(id="staff-uuid") try: bio = staff.biography except Biography.DoesNotExist: print("No biography found") # Find all staff with biographies in Spanish spanish_providers = CustomStaff.objects.filter( biography__language="Spanish" ) # Find staff whose biography was last updated before a certain date from datetime import datetime, timedelta outdated_bios = CustomStaff.objects.filter( biography__last_modified_at__lte=datetime.now() - timedelta(days=365) ) ``` * * * ## One-to-Many Relationships A one-to-many (or many-to-one) relationship allows one record to be associated with multiple records in another model. Use `ForeignKey` to define this relationship. ### Basic One-to-Many ```python from canvas_sdk.v1.data import Staff, ModelExtension from canvas_sdk.v1.data.base import CustomModel from django.db.models import CASCADE, DateTimeField, DecimalField, ForeignKey, TextField class CustomStaff(Staff, ModelExtension): """Extends Staff with custom attribute support.""" pass class Biography(CustomModel): biography = TextField() language = TextField() version = DecimalField(default=1.0, decimal_places=1, max_digits=3) last_modified_at = DateTimeField(auto_now_add=True) # Same as one-to-one, but a Foreign key with a plural 'related_name'. Now, each staff may have multiple biographies, # perhaps in different languages. staff = ForeignKey( CustomStaff, to_field="dbid", on_delete=CASCADE, related_name="biographies" ) ``` ### Creating One-to-Many Records ```python from my_plugin.models import CustomStaff, Biography # Get staff member staff = CustomStaff.objects.get(id="staff-uuid") # Create multiple biographies for one provider (e.g., in different languages) english_bio = Biography.objects.create( staff=staff, biography="Dr. Smith is a board-certified cardiologist with over 20 years of experience in interventional cardiology.", language="English", version=1.0 ) spanish_bio = Biography.objects.create( staff=staff, biography="La Dra. Smith es una cardióloga certificada con más de 20 años de experiencia en cardiología intervencionista.", language="Spanish", version=1.0 ) ``` ### Querying One-to-Many Relationships ```python from my_plugin.models import CustomStaff, Biography # Access from biography to staff (forward) biography = Biography.objects.get(language="Spanish") provider = biography.staff print(f"Provider: {provider.first_name} {provider.last_name}") # Access from staff to biographies (reverse, using related_name) staff = CustomStaff.objects.get(id="staff-uuid") biographies = staff.biographies.all() for bio in biographies: print(f"- {bio.language}: {bio.biography[:50]}... (v{bio.version})") # Filter reverse relationship english_bios = staff.biographies.filter(language="English") # Query across relationship # Find all staff who have biographies in Spanish spanish_speaking_providers = CustomStaff.objects.filter( biographies__language="Spanish" ) # Find staff with multiple biography versions from django.db.models import Count providers_with_multiple_bios = CustomStaff.objects.annotate( bio_count=Count('biographies') ).filter(bio_count__gt=1) # Count related records biography_count = staff.biographies.count() # Check existence has_spanish_bio = staff.biographies.filter(language="Spanish").exists() ``` * * * ## Many-to-Many Relationships A many-to-many relationship allows multiple records in one model to be associated with multiple records in another model. Many-to-many relationships require an **explicit through model** — a CustomModel that contains ForeignKey fields to both sides of the relationship. Standard Django allows `ManyToManyField` to create an implicit join table automatically, but the Canvas SDK does not support implicit through tables because each table must be a CustomModel with a managed schema lifecycle. You can define the relationship in two ways: 1. **Through model only** — Define the through model with ForeignKeys and query through it directly. 2. **Through model +`ManyToManyField`** — Add a `ManyToManyField` with an explicit `through` parameter for cleaner ORM access. Both approaches create the same database tables. The `ManyToManyField` adds ORM convenience (e.g., `specialty.staff.all()` instead of traversing the join table manually) but does not change the underlying schema. ### Through Model Only The simplest approach is to define just the through model. This works well when the through model has additional metadata fields or when you prefer to query the join table directly. ```python from django.db.models import CASCADE, ForeignKey, Index, TextField, UniqueConstraint from canvas_sdk.v1.data.base import CustomModel from canvas_sdk.v1.data import Staff, ModelExtension class CustomStaff(Staff, ModelExtension): """Extends Staff with custom attribute support.""" pass class Specialty(CustomModel): """Medical specialty (e.g., Cardiology, Neurology).""" name = TextField() class Meta: indexes = [ Index(fields=["name"]), ] # Declaring this class will result in a join table called `staffspecialty` class StaffSpecialty(CustomModel): """Many-to-many relationship: Staff can have many specialties, specialties can have many staff.""" staff = ForeignKey( CustomStaff, to_field="dbid", on_delete=CASCADE, related_name="staff_specialties" ) specialty = ForeignKey( Specialty, to_field="dbid", on_delete=CASCADE, related_name="staff_specialties" ) class Meta: constraints = [ UniqueConstraint( fields=["staff", "specialty"], name="unique_staff_specialty", ), ] ``` This creates a many-to-many relationship where: - One staff member can have multiple specialties - One specialty can be assigned to multiple staff members - `StaffSpecialty` is the through model that connects them **Preventing duplicate associations:** Through models typically need a uniqueness constraint on the pair of foreign key columns to prevent the same association from being created twice. Add a `UniqueConstraint` to the through model's `Meta.constraints` referencing both FK field names (e.g., `staff` and `specialty`). Without this, calling `StaffSpecialty.objects.create(staff=staff, specialty=cardiology)` twice would create two identical rows. See Uniqueness Constraints for more details on constraint naming and lifecycle. ### Through Model + ManyToManyField Adding a `ManyToManyField` with an explicit `through` parameter gives you direct ORM access to the related objects without manually traversing the join table. **Important:** The `through` parameter is **required**. A `ManyToManyField` without `through` will cause an error because the SDK cannot manage implicit join tables. ```python from django.db.models import CASCADE, ForeignKey, Index, ManyToManyField, TextField, UniqueConstraint from canvas_sdk.v1.data.base import CustomModel from canvas_sdk.v1.data import Staff, ModelExtension class CustomStaff(Staff, ModelExtension): """Extends Staff with custom attribute support.""" pass class Specialty(CustomModel): """Medical specialty (e.g., Cardiology, Neurology).""" name = TextField() staff = ManyToManyField( CustomStaff, through="StaffSpecialty", related_name="%(app_label)s_specialties", ) class Meta: indexes = [ Index(fields=["name"]), ] class StaffSpecialty(CustomModel): """Through model for the staff-specialty relationship.""" staff = ForeignKey( CustomStaff, to_field="dbid", on_delete=CASCADE, related_name="%(app_label)s_staff_specialties", ) specialty = ForeignKey( Specialty, to_field="dbid", on_delete=CASCADE, related_name="staff_specialties", ) class Meta: constraints = [ UniqueConstraint( fields=["staff", "specialty"], name="unique_staff_specialty", ), ] ``` With the `ManyToManyField` declared, you can traverse the relationship directly: ```python # Direct access to related objects (returns Staff queryset, not StaffSpecialty) specialty = Specialty.objects.get(name="Cardiology") staff_members = specialty.staff.all() # Reverse access from staff to specialties staff = CustomStaff.objects.get(id="staff-uuid") specialties = staff.staff_plus_specialties.all() # uses the ManyToManyField's related_name ``` Compare this with the through-model-only approach, where you must navigate through the join table: ```python # Without ManyToManyField — must traverse the join table staff_members = [ss.staff for ss in specialty.staff_specialties.all()] ``` #### Differences from Standard Django ManyToManyField Behavior| Standard Django| Canvas SDK ---|---|--- `through` parameter| Optional — Django creates an implicit join table| **Required** — must reference a CustomModel `.add()`, `.remove()`, `.set()`| Available when no explicit through model| **Not available** — use the through model's `.objects.create()` and `.delete()` instead `.clear()`| Available| **Not available** — use `StaffSpecialty.objects.filter(...).delete()` instead `.all()`, filtering, `prefetch_related`| Available| Available Because Django requires you to use the through model directly for creating and deleting relationships when an explicit `through` is declared, the CRUD patterns are the same whether or not you add the `ManyToManyField`. The field's value is in query convenience — direct `.all()` access and cleaner `prefetch_related` lookups. #### related_name with ManyToManyField When a `ManyToManyField` targets a core SDK model (like `Staff` or `Patient`), you **must** use the `%(app_label)s_` prefix in `related_name` to avoid naming collisions between plugins: ```python staff = ManyToManyField( CustomStaff, through="StaffSpecialty", related_name="%(app_label)s_specialties", # becomes e.g. "my_plugin_specialties" ) ``` This is the same namespacing requirement that applies to `ForeignKey` and `OneToOneField` when targeting SDK models. See [Extending SDK Models](/sdk/custom-data-extending-sdk-models/) for a full explanation of when namespacing is required and how proxy models avoid it. ### Creating Many-to-Many Records Regardless of whether you use `ManyToManyField`, create and delete relationships through the through model directly: ```python from my_plugin.models import CustomStaff, Specialty, StaffSpecialty # Create specialties cardiology = Specialty.objects.create(name="Cardiology") internal_medicine = Specialty.objects.create(name="Internal Medicine") emergency_medicine = Specialty.objects.create(name="Emergency Medicine") # Get staff member staff = CustomStaff.objects.get(id="staff-uuid") # Create associations between staff and specialties StaffSpecialty.objects.create(staff=staff, specialty=cardiology) StaffSpecialty.objects.create(staff=staff, specialty=internal_medicine) # Bulk create multiple associations at once specialties_to_add = [emergency_medicine, cardiology] staff_specialties = [ StaffSpecialty(staff=staff, specialty=specialty) for specialty in specialties_to_add ] StaffSpecialty.objects.bulk_create(staff_specialties) # Replace all specialties for a staff member # First, remove existing associations StaffSpecialty.objects.filter(staff=staff).delete() # Then create new associations new_specialties = [cardiology, emergency_medicine] new_staff_specialties = [ StaffSpecialty(staff=staff, specialty=specialty) for specialty in new_specialties ] StaffSpecialty.objects.bulk_create(new_staff_specialties) ``` **Note:** Do not use `.add()`, `.remove()`, `.set()`, or `.clear()` on the `ManyToManyField`. Django disables these methods when an explicit `through` model is declared. Use the through model's manager (e.g., `StaffSpecialty.objects`) for all create and delete operations. ### Querying Many-to-Many Relationships ```python from my_plugin.models import CustomStaff, Specialty, StaffSpecialty # Access staff member's specialties through the join table staff = CustomStaff.objects.get(id="staff-uuid") staff_specialty_records = staff.staff_specialties.all() for staff_specialty in staff_specialty_records: print(f"- {staff_specialty.specialty.name}") # Get just the specialty names specialty_names = [ss.specialty.name for ss in staff.staff_specialties.all()] # Access all staff members with a specific specialty (reverse) cardiology = Specialty.objects.get(name="Cardiology") cardiology_staff_records = cardiology.staff_specialties.all() for staff_specialty in cardiology_staff_records: staff_member = staff_specialty.staff print(f"- {staff_member.first_name} {staff_member.last_name}") # Find staff IDs with specific specialties staff_ids = StaffSpecialty.objects.filter( specialty__name__in=["Cardiology", "Internal Medicine"] ).values_list("staff_id", flat=True) # Find staff members with a specific specialty cardiologists = CustomStaff.objects.filter( staff_specialties__specialty__name="Cardiology" ).distinct() # Check if a staff member has a specific specialty has_cardiology = staff.staff_specialties.filter(specialty__name="Cardiology").exists() # Count specialties for a staff member specialty_count = staff.staff_specialties.count() # Efficient querying with prefetch_related staff_with_specialties = ( CustomStaff.objects .prefetch_related("staff_specialties__specialty") .all() ) for staff in staff_with_specialties: specialties = [ss.specialty.name for ss in staff.staff_specialties.all()] print(f"{staff.first_name} {staff.last_name}: {', '.join(specialties)}") ``` **Key points about many-to-many relationships:** - Both sides of the relationship can access the through model using `related_name` - Without `ManyToManyField`: `staff.staff_specialties.all()` returns `StaffSpecialty` objects — access the related object via `ss.specialty` - With `ManyToManyField`: `specialty.staff.all()` returns `Staff` objects directly - You can add additional fields to the through model to store metadata about the relationship (e.g., date assigned, certification level, etc.) - Query across the relationship using double underscores: `CustomStaff.objects.filter(staff_specialties__specialty__name="Cardiology")` ## Delete Behavior The `on_delete` parameter on `ForeignKey` and `OneToOneField` controls what happens to child records when a parent record is deleted. The SDK supports three values: - **`CASCADE`** — Delete the child record automatically. This is the most common choice for tightly-coupled relationships like join table entries, child records that have no meaning without their parent, or `OneToOneField` with `primary_key=True`. - **`SET_NULL`** — Set the foreign key column to `NULL`, keeping the child record. Useful when the child has independent value even if its parent is removed (e.g., an audit log entry whose associated staff member has been deactivated). - **`DO_NOTHING`** — Take no automatic action. The plugin is fully responsible for preventing orphaned references. These behaviors are enforced at the Django ORM level, not by database-level foreign key constraints. They apply when deleting via `model.delete()` or `queryset.delete()`. **Tip:** Use `CASCADE` on through-model (join table) foreign keys so that deleting either side of a many-to-many relationship automatically cleans up the association rows. ## The CustomModel Lifecycle Managing database schemas necessarily introduces complexity, because there is state to maintain over time as the software evolves. Common pitfalls include expensive table rewrite operations, migrations that fail in some environments due to manual changes, database system-specific nuances, unsatisfied foreign key constraints due to data corruption or improper order of operations, etc. The Canvas SDK Custom Data feature aims to simplify maintenance, while sacrificing some rigor found in a full migration system like Django's. Operation| Allowed| Explanation ---|---|--- Create Model| Yes| A table corresponding to your CustomModel will be created if it does not exist. An autoincrementing column named `dbid` will be its sole attribute. Add Field| Yes| A column corresponding to a Field declared within your CustomModel will be added to the table if it does not exist. It will be nullable, without defaults to eliminate table rewrites. Add UniqueConstraint| Yes| A unique index will be created if it does not already exist. Fails if existing data contains duplicates for the constrained columns. Add Index| Yes| An index will be created if it does not already exist. Alter Field| No| This can cause a table rewrite, and requires a full migration metadata system. Create a new Field in your model. Copy data from old to new. Drop Field| No| This will cause a table rewrite, and requires a full migration metadata system. Remove the Field from your model and it will be ignored. Drop UniqueConstraint| No| Remove the constraint from your model and it will be ignored, but the unique index will remain in the database. Drop Index| No| Remove the index from your model and it will be ignored, but the index will remain in the database. Alter Model| No| Requires a full migration metadata system. Create a new Model in your plugin. Copy data from old to new. Drop Model| No| Requires a full migration metadata system. Remove the model from your plugin and it will be ignored. ### Best Practices 1. Emphasize local development over use of a development EMR instance. 2. Write [automated tests](/sdk/custom-data-testing/) exercising your business logic. 3. Extract business logic and CRUD operations into "service" classes that can be tested in isolation. ## Advanced Patterns ### Combining Approaches You can combine CustomModels with [AttributeHubs](/sdk/custom-data-attribute-hubs/) for maximum flexibility: ```python from canvas_sdk.v1.data.base import CustomModel from canvas_sdk.v1.data import AttributeHub, Staff, ModelExtension from django.db.models import CASCADE, ForeignKey, SET_NULL, TextField class CustomStaff(Staff, ModelExtension): pass class Department(CustomModel): """Structured department model.""" name = TextField() code = TextField() class StaffDepartment(CustomModel): """Staff can belong to multiple departments.""" staff = ForeignKey( CustomStaff, on_delete=SET_NULL, related_name="department_assignments" ) department = ForeignKey( Department, on_delete=CASCADE, related_name="staff_members" ) role = TextField() # Use CustomModels for structured data with relationships staff = CustomStaff.objects.get(id="staff-uuid") dept = Department.objects.get(code="CARDIO") StaffDepartment.objects.create( staff=staff, department=dept, role="Lead Physician" ) # Use an AttributeHub for flexible, unstructured data hub, created = AttributeHub.objects.get_or_create( type="staff_preferences", id=f"staff:{staff.id}" ) hub.set_attributes({ "pager_number": "555-1234", "preferred_contact": "email", "office_hours": {"monday": "9-5", "tuesday": "9-5"} }) ``` ### Query Optimization Optimize database queries using `select_related` and `prefetch_related`: ```python from my_plugin.models import Specialty, StaffSpecialty, CustomStaff # Use select_related for ForeignKey (SQL JOIN) # Load StaffSpecialty with related staff and specialty in one query staff_specialties = StaffSpecialty.objects.select_related("staff", "specialty").all() for ss in staff_specialties: # No additional queries - both staff and specialty are already loaded print(f"{ss.staff.first_name} {ss.staff.last_name}: {ss.specialty.name}") # Use prefetch_related for reverse ForeignKey relationships # Load staff with all their specialties efficiently staff_list = CustomStaff.objects.prefetch_related("staff_specialties__specialty").all() for staff in staff_list: # No additional queries - staff_specialties and specialties are already loaded for ss in staff.staff_specialties.all(): print(f"{staff.first_name}: {ss.specialty.name}") # Prefetch specialties for multiple staff members specialties_list = Specialty.objects.prefetch_related("staff_specialties__staff").all() for specialty in specialties_list: staff_members = [ss.staff for ss in specialty.staff_specialties.all()] print(f"{specialty.name}: {len(staff_members)} staff members") # Use Prefetch for custom filtering from django.db.models import Prefetch # Only load staff specialties with specific specialty names staff_with_filtered_specialties = CustomStaff.objects.prefetch_related( Prefetch( "staff_specialties", queryset=StaffSpecialty.objects.filter( specialty__name__in=["Cardiology", "Neurology"] ).select_related("specialty") ) ).all() ``` ### Complex Queries Use Django's Q objects for complex filtering and aggregation: ```python from django.db.models import Q, Count from my_plugin.models import CustomStaff, Specialty, StaffSpecialty # OR conditions - Find staff with Cardiology OR Neurology specialty staff_with_cardio_or_neuro = CustomStaff.objects.filter( Q(staff_specialties__specialty__name="Cardiology") | Q(staff_specialties__specialty__name="Neurology") ).distinct() # AND conditions - Find specialties with "Cardiology" or "Medicine" in the name cardio_or_medicine = Specialty.objects.filter( Q(name__icontains="Cardiology") | Q(name__icontains="Medicine") ) # Negation - Find staff WITHOUT a specific specialty staff_without_cardiology = CustomStaff.objects.exclude( staff_specialties__specialty__name="Cardiology" ) # Complex filtering - Staff with multiple specific specialties # Note: This requires DISTINCT because joins can create duplicate rows staff_with_multiple = CustomStaff.objects.filter( staff_specialties__specialty__name="Cardiology" ).filter( staff_specialties__specialty__name="Internal Medicine" ).distinct() # Count related objects - Staff with specialty counts staff_with_counts = CustomStaff.objects.annotate( specialty_count=Count("staff_specialties") ).filter(specialty_count__gte=2) # Group by and aggregate - Count how many staff have each specialty specialty_counts = Specialty.objects.annotate( staff_count=Count("staff_specialties") ).order_by("-staff_count") for specialty in specialty_counts: print(f"{specialty.name}: {specialty.staff_count} staff members") ``` ## Best Practices ### Model Design 1. **Use appropriate field types** \- Choose the most specific field type for your data 2. **Define related_name** \- Always specify `related_name` for clear reverse relationships 3. **Keep models focused** \- Each model should represent a single, well-defined concept ### Relationships 1. **Choose the right relationship type** \- OneToOne for 1:1, ForeignKey for 1:many, join tables and "through" models for many:many 2. **Use through models** \- To create a join table bridging two other entities, create a CustomModel representing the relationship 3. **Handle deletions** \- Use `CASCADE` on join table foreign keys so associations are cleaned up automatically. Use `SET_NULL` when child records should survive parent deletion. Use `DO_NOTHING` only when you manage cleanup explicitly in plugin code ### Performance 1. **Add indexes strategically** \- Index frequently filtered fields - foreign key fields are automatically indexed 2. **Use select_related** \- For ForeignKey and OneToOneField to reduce queries 3. **Use prefetch_related** \- For reverse ForeignKey fields (including join tables for many-to-many fields) 4. **Avoid N+1 queries** \- Always prefetch related data when iterating 5. **Use exists() for checks** \- More efficient than count() or len() 6. **Use iterator() for large datasets** \- Reduces memory usage for processing many records ### Data Integrity 1. **Enforce uniqueness with UniqueConstraint** \- Use `UniqueConstraint` in `Meta.constraints` to prevent duplicate data at the database level 2. **Validate in model methods** \- Add custom validation in `clean()` method 3. **Use transactions** \- Wrap multiple operations in atomic transactions 4. **Handle DoesNotExist** \- Always catch exceptions when using `get()` ### Testing 1. **Use model factories** \- Create test data with factory patterns 2. **Test model methods** \- Verify custom model methods and properties 3. **Test relationships** \- Ensure relationships work in both directions 4. **Test data quality** \- The plugin is responsible for ensuring uniqueness and validity of foreign keys 5. **Test edge cases** \- Test with null values, empty strings, boundary conditions ## See Also - [Custom Data Overview](/sdk/custom-data/) \- Overview of all custom data techniques - [Extending SDK Models](/sdk/custom-data-extending-sdk-models/) \- Proxy models, `related_name` namespacing, and referencing SDK models - [Design Considerations](/sdk/custom-data-design-considerations/) \- Choosing the right technique and avoiding anti-patterns - [AttributeHubs](/sdk/custom-data-attribute-hubs/) \- Standalone key-value storage - [Transactions](/sdk/custom-data-transactions/) \- All-or-nothing writes with `transaction.atomic()` - [Sharing Data](/sdk/custom-data-sharing-data/) \- Sharing data among plugins - [Testing Custom Data](/sdk/custom-data-testing/) \- Testing utilities and examples - [Data Models](/sdk/data/) \- Core SDK data models - [Caching API](/sdk/caching) \- Auto-expiring transient data --- # Design Considerations Source: https://docs.canvasmedical.com/sdk/custom-data-design-considerations/ Choosing the right storage technique prevents performance problems, data inconsistencies, and unnecessary code complexity down the road. This page describes common anti-patterns for each technique and recommends alternatives. For an overview of available techniques, see the [Custom Data Overview](/sdk/custom-data/). ## Extending SDK Models with Custom Data To attach custom fields to existing SDK models (Patient, Staff, etc.), use a [CustomModel](/sdk/custom-data-custom-models/) with a `OneToOneField` pointing at the SDK model. This gives you typed, indexed columns with full ORM support — `select_related`, reverse lookups via `related_name`, and compound filtering in a single query. ```python from canvas_sdk.v1.data import Patient, ModelExtension from canvas_sdk.v1.data.base import CustomModel from django.db.models import BooleanField, DO_NOTHING, IntegerField, OneToOneField, TextField class CustomPatient(Patient, ModelExtension): pass class PatientProfile(CustomModel): patient = OneToOneField( CustomPatient, to_field="dbid", on_delete=DO_NOTHING, related_name="profile" ) preferred_language = TextField() risk_score = IntegerField() is_vip = BooleanField() ``` CustomModels with `OneToOneField` are preferred because they offer typed columns, indexing, compound queries, and a schema that is visible and self-documenting. See [Extending SDK Models](/sdk/custom-data-extending-sdk-models/) for details on proxy models and `related_name` namespacing. For truly simple, one-off metadata that doesn't justify a table (e.g., a single configuration flag), an [AttributeHub](/sdk/custom-data-attribute-hubs/) can be a lighter-weight alternative. ## AttributeHubs — When to Reconsider AttributeHubs use EAV (entity-attribute-value) storage and are standalone — not attached to any Canvas model. They are convenient for one-off state and configuration, but the same EAV limitations apply when used at scale. Their best application is storing a collection of attributes that will mainly be retrieved by identifier rather than by value. ### Modeling entities with relationships If you have "departments" and need to assign staff to them, encoding `staff_id` as a string attribute means no JOINs, no referential integrity, potentially duplicated data. The plugin must manually maintain consistency. **Use instead:** [CustomModels](/sdk/custom-data-custom-models/) with `ForeignKey` fields and junction tables handle relationships naturally, with ORM-level traversal and `prefetch_related` support. ### Large homogeneous collections Storing thousands of hubs of `type="patient_visit"` where you need to filter, sort, or paginate across them becomes expensive. Each filter condition requires a JOIN to the attribute table. **Use instead:** A [CustomModel](/sdk/custom-data-custom-models/) with typed, indexed columns. Filtering, sorting, and pagination use standard SQL operations. ### Data requiring aggregation Trying to SUM, AVG, or COUNT across AttributeHub attributes requires joining to the attribute table and selecting the correct typed column (`int_value`, `decimal_value`, etc.) per attribute name. This is fragile and slow. **Use instead:** [CustomModel](/sdk/custom-data-custom-models/) columns make Django ORM aggregation (`annotate`, `aggregate`) straightforward. ### Data with a consistent schema If every hub of a given `type` has the same set of attributes, you've designed a schema — just without enforcement or indexes. You're paying the cost of EAV without the benefit of flexibility. **Use instead:** A [CustomModel](/sdk/custom-data-custom-models/) gives you type safety, column-level indexes, and cleaner queries. ## CustomModels — When to Reconsider CustomModels create real database tables with typed columns. They are the most powerful option but carry a commitment: tables can be added but never dropped via the SDK, and fields can be added but never altered or removed. ### Simple metadata on existing models For a small number of independent metadata fields on an SDK model (e.g., a single `is_vip` flag on Patient), a full CustomModel with `OneToOneField` is the recommended approach — it gives you typed columns, indexing, and compound queries. However, if the overhead of a table feels excessive for truly one-off data, an [AttributeHub](/sdk/custom-data-attribute-hubs/) keyed by entity type and ID can serve as a lightweight alternative. ### Highly dynamic or schemaless data If every record has different fields — for example, caching responses from external APIs where the payload varies per endpoint — a CustomModel forces a rigid schema. You'll accumulate nullable columns for each variation, and fields can never be dropped. **Use instead:** [AttributeHubs](/sdk/custom-data-attribute-hubs/) for truly schemaless data, or a CustomModel with a single `JSONField` if you still want a table but need flexible contents. ### Ephemeral data CustomModel tables are permanent. Once created, they cannot be dropped via the SDK. For short-lived data like session tokens, rate-limit windows, or temporary processing state, a persistent table is the wrong tool. **Use instead:** The [Caching API](/sdk/caching) for data with a natural TTL. For semi-persistent unstructured state, [AttributeHubs](/sdk/custom-data-attribute-hubs/) are lighter weight. ### Premature normalization Don't create five interrelated CustomModels with foreign keys when the data is simple and queried infrequently. Over-engineering the schema early is costly because tables cannot be dropped if you change your mind. **Use instead:** Start with fewer models. A single `JSONField` column or an [AttributeHub](/sdk/custom-data-attribute-hubs/) can hold loosely structured data until access patterns stabilize and justify a richer schema. ## Quick Reference Situation| Recommended Approach ---|--- Custom fields on Patient, Staff, or other SDK models| CustomModel with `OneToOneField` Provider preferences (notification settings, display options)| CustomModel with `OneToOneField` API sync cursors, external system state| AttributeHub Plugin configuration or feature flags| AttributeHub One-off key-value data unrelated to a Canvas model| AttributeHub Rapid prototyping before committing to a schema| AttributeHub Structured entities with a stable, known schema| CustomModel Relationships between entities (foreign keys, join tables)| CustomModel Data requiring compound filtering, sorting, or aggregation| CustomModel Data consumed by reports or analytics| CustomModel High-write-frequency counters or accumulators| CustomModel Short-lived data that should auto-expire| [Caching API](/sdk/caching) ## See Also - [Custom Data Overview](/sdk/custom-data/) \- Introduction to custom data storage - [Extending SDK Models](/sdk/custom-data-extending-sdk-models/) \- Proxy models and referencing SDK models - [AttributeHubs](/sdk/custom-data-attribute-hubs/) \- Standalone key-value storage - [CustomModels](/sdk/custom-data-custom-models/) \- Django models for structured data - [Transactions](/sdk/custom-data-transactions/) \- All-or-nothing writes with `transaction.atomic()` - [Caching API](/sdk/caching) \- Auto-expiring transient data --- # Extending SDK Models Source: https://docs.canvasmedical.com/sdk/custom-data-extending-sdk-models/ ## Overview SDK models like `Patient`, `Staff`, and `Note` are shared across all plugins. To attach custom data or create relationships to these models from your [CustomModels](/sdk/custom-data-custom-models/), extend them with `ModelExtension` to create a plugin-private proxy model. * * * ## Creating a Model Extension Subclass the SDK model together with `ModelExtension`: ```python from canvas_sdk.v1.data import Staff, ModelExtension class CustomStaff(Staff, ModelExtension): pass ``` What happens automatically: - **`proxy = True`** is set by the `ModelExtensionMetaClass`. No new database table is created — the proxy shares the parent model's table. - **`app_label`** is set to your plugin name (derived from the module path). - `CustomStaff` behaves identically to `Staff` for queries — `CustomStaff.objects.all()` returns the same rows as `Staff.objects.all()`. You can name the class anything, but it **must** subclass both a concrete SDK model and `ModelExtension`. Extended SDK models may be defined anywhere in your plugin since they do not require database modifications. However, placing them in the `models` directory alongside your CustomModels is recommended for clarity. * * * ## Why Proxy Models? SDK models are shared across every plugin in the system. If two plugins each added a bare `related_name="biography"` on a `ForeignKey` pointing at `Staff`, Django would raise a clash error — both reverse relations would compete for the same attribute on the shared `Staff` class. Proxy models solve this by giving each plugin its own private subclass of the SDK model. Because `CustomStaff` is a distinct model (even though it shares the same table), reverse relations registered on `CustomStaff` are scoped to the plugin that defined it. The shared SDK model stays clean and unaffected. * * * ## Referencing SDK Models from CustomModels When a CustomModel needs a `ForeignKey` or `OneToOneField` pointing at an SDK model, you have two approaches. ### Approach 1: Via Proxy (Recommended) Create a `ModelExtension` proxy and point your relationship field at it. Because the target is plugin-private, `related_name` can be any simple name — no namespacing required. ```python from canvas_sdk.v1.data import Staff, ModelExtension from canvas_sdk.v1.data.base import CustomModel from django.db.models import DO_NOTHING, OneToOneField, TextField class CustomStaff(Staff, ModelExtension): pass class Biography(CustomModel): staff = OneToOneField( CustomStaff, to_field="dbid", on_delete=DO_NOTHING, related_name="biography" ) text = TextField() ``` Reverse lookup works through the proxy: ```python staff = CustomStaff.objects.get(id="some-uuid") bio = staff.biography # accesses the Biography via related_name ``` ### Approach 2: Direct SDK Model with Namespaced `related_name` Point directly at the SDK model, but you **must** namespace the `related_name` to prevent collisions across plugins. Two formats are accepted: Format| Example| Notes ---|---|--- `%(app_label)s_` prefix (recommended)| `related_name="%(app_label)s_biography"`| Django substitutes your plugin's `app_label` at class creation time Hardcoded plugin prefix| `related_name="my_plugin_biography"`| Works, but breaks if you rename the plugin `"+"`| `related_name="+"`| Disables the reverse relation entirely ```python from canvas_sdk.v1.data import Staff from canvas_sdk.v1.data.base import CustomModel from django.db.models import DO_NOTHING, OneToOneField, TextField class Biography(CustomModel): staff = OneToOneField( Staff, to_field="dbid", on_delete=DO_NOTHING, related_name="%(app_label)s_biography" ) text = TextField() ``` ### Comparison | Via Proxy| Direct SDK Model ---|---|--- `related_name` namespacing required?| No| Yes Reverse lookup available?| Yes, via the proxy class| Yes, via the SDK model Reverse attribute name| Simple (e.g., `staff.biography`)| Prefixed (e.g., `staff.my_plugin_biography`) Extra class needed?| Yes (`ModelExtension` proxy)| No In most cases, Approach 1 is preferred — it keeps `related_name` values short and readable, and the proxy class is reusable across multiple CustomModels in the same plugin. * * * ## Proxying Related Objects with `proxy_field` When you use `ModelExtension` proxies and follow related objects through ForeignKey fields, the returned instance is the base SDK class — not your proxy. For example: ```python from canvas_sdk.v1.data import Note, Patient, ModelExtension class CustomPatient(Patient, ModelExtension): def full_display_name(self): # custom method only available on CustomPatient return f"{self.first_name} {self.last_name} (DOB: {self.birth_date})" class CustomNote(Note, ModelExtension): pass note = CustomNote.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") note.patient # returns a Patient, not CustomPatient note.patient.full_display_name() # AttributeError! ``` This happens because Django's ForeignKey descriptor resolves the relation to the concrete model (`Patient`), unaware of your proxy class. You would need an extra query to "re-fetch" the object as a `CustomPatient`. ### The `proxy_field` descriptor `proxy_field` solves this by intercepting the ForeignKey access and transparently returning the proxy class instead: ```python from canvas_sdk.v1.data import Note, Patient, ModelExtension, proxy_field class CustomPatient(Patient, ModelExtension): def full_display_name(self): return f"{self.first_name} {self.last_name} (DOB: {self.birth_date})" class CustomNote(Note, ModelExtension): patient = proxy_field(CustomPatient) note = CustomNote.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") note.patient # returns a CustomPatient instance note.patient.full_display_name() # works! ``` No extra queries are issued — `proxy_field` reuses the already-fetched row and swaps its Python class to the proxy. Because proxy models share the same database table, this is safe and efficient. ### How it works `proxy_field` is a Python [descriptor](https://docs.python.org/3/howto/descriptor.html). When you declare `patient = proxy_field(CustomPatient)` on a model class: 1. `__set_name__` runs at class creation time and finds the original FK descriptor (`patient`) from the parent class in the MRO. 2. `__get__` delegates to that original descriptor to load the related object, then sets `__class__` on the result to your proxy class. 3. `__set__` passes assignment through to the original descriptor, so `note.patient = some_patient` continues to work normally. 4. Accessing the attribute on the class (e.g., `CustomNote.patient`) returns the descriptor itself, not a model instance. ### When to use `proxy_field` Use `proxy_field` when: - You have `ModelExtension` proxies for multiple SDK models and need to navigate between them while keeping access to your custom methods or `related_name` fields. - You want to avoid extra database queries to "re-fetch" a related object as the proxy type. `proxy_field` is not needed when: - You don't add custom methods, properties or `related_name` fields to your proxy class. - You access the related object's fields directly (e.g., `note.patient.first_name`) without needing proxy-specific behavior. ### Null foreign keys `proxy_field` handles nullable ForeignKeys safely — if the relation is `None`, it returns `None` without error: ```python note = CustomNote.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") note.patient # returns None if the FK is null, not an error ``` * * * ## Common Errors ### `ValueError`: non-namespaced `related_name` on SDK model target If you point a `ForeignKey` or `OneToOneField` directly at an SDK model with a plain `related_name`, installation will fail with: ```text ValueError: CustomModel 'Biography' declares related_name='biography' on field 'staff' targeting SDK model 'Staff'. To prevent collisions across plugins, use a namespaced related_name like related_name='%(app_label)s_biography', or related_name='+' to disable the reverse relation. ``` **Fix:** Either switch to a proxy target (Approach 1) or add the `%(app_label)s_` prefix to your `related_name` (Approach 2). This validation applies to `ForeignKey` and `OneToOneField`. Fields targeting other CustomModels or proxy models are exempt because those targets are already plugin-private. * * * ## See Also - [CustomModels](/sdk/custom-data-custom-models/) \- Defining structured models, relationships, and queries - [AttributeHubs](/sdk/custom-data-attribute-hubs/) \- Standalone key-value storage - [Design Considerations](/sdk/custom-data-design-considerations/) \- Choosing the right technique and avoiding anti-patterns - [Transactions](/sdk/custom-data-transactions/) \- All-or-nothing writes with `transaction.atomic()` - [Sharing Data](/sdk/custom-data-sharing-data/) \- Sharing data with other plugins and external services - [Data Models](/sdk/data/) \- Core SDK data models --- # Namespace Lifecycle Source: https://docs.canvasmedical.com/sdk/custom-data-namespace-lifecycle/ Every plugin that uses custom data operates within a **namespace** — an isolated PostgreSQL schema that holds the plugin's AttributeHubs and CustomModels. During iterative development, namespaces accumulate tables, columns, and data that can get in the way. This page explains how namespaces are created and managed, and how to use the Canvas CLI to inspect and clean up namespaces as you work. ## Namespace Naming Rules Namespace names use the format `org__name` (two parts separated by a double underscore). Each part must start with a lowercase letter and contain only lowercase letters, digits, and single underscores. The total length must not exceed **63 characters** (PostgreSQL's identifier limit). Examples of valid names: `acme__shared_data`, `myorg__analytics` ## How Namespaces Are Created A namespace is created automatically when the **first plugin** with `"access": "read_write"` is installed into it. The installation process: 1. Creates a PostgreSQL schema named after the namespace 2. Establishes two authentication keys (`namespace_read_access_key` and `namespace_read_write_access_key`) — auto-generated UUIDs by default, or values supplied by the installer (see [Pre-Supplying Keys at Creation](/sdk/custom-data-sharing-data/#pre-supplying-keys-at-creation)) 3. Stores the keys as secrets in the plugin 4. Creates any CustomModel tables defined by the plugin Subsequent plugins can join the namespace with the appropriate access key. See [Sharing Data](/sdk/custom-data-sharing-data/) for details on multi-plugin namespaces. ### Installation Flow ![](/assets/images/sdk/custom-data/installation_flowchart.jpg) The path depends on the declared access level and whether the namespace already exists: Declared Access| Namespace Exists| Key Provided| Result ---|---|---|--- `read_write`| No| Neither| Creates namespace and tables; auto-generates both keys `read_write`| No| Both `namespace_read_access_key` and `namespace_read_write_access_key`| Creates namespace and tables using the supplied keys (see [Pre-Supplying Keys at Creation](/sdk/custom-data-sharing-data/#pre-supplying-keys-at-creation)) `read_write`| Yes| Valid `namespace_read_write_access_key`| Plugin installed, tables created `read_write`| Yes| Invalid or missing| Installation fails `read`| Yes| Valid `namespace_read_access_key`| Plugin installed with read access `read`| No| N/A| Installation fails `read`| Yes| Invalid or missing| Installation fails ## Development Workflow When developing a plugin with custom data, you'll typically iterate through cycles of changing your models, reinstalling the plugin, and testing. Each reinstall can leave behind tables from previous iterations — renamed models leave orphaned tables, and test data accumulates. The `canvas namespace` CLI commands let you inspect what's in a namespace and clean it up without having to connect to the database directly. ### Typical Iteration Cycle 1. Edit your CustomModel definitions or manifest 2. Redeploy the plugin: `canvas deploy acme__my_plugin --instance dev-instance` 3. Test your changes 4. If models were renamed or removed, use `canvas namespace reset` to clean up orphaned tables 5. Repeat ## CLI Commands All namespace commands require a running Canvas instance. Pass `--host` to specify which instance to connect to. They connect with the [instance API credentials](/sdk/canvas_cli/#instance-api-credentials-credentialsini-deprecated) in `~/.canvas/credentials.ini` until they move to your Canvas Platform sign-in, before December 14, 2026. ### Listing Namespaces See all custom data namespaces on an instance: ```bash canvas namespace list --host dev-instance ``` Output shows each namespace with its total table count and the number of custom (non-system) tables: ```text acme_corp__shared_data tables: 7 custom: 3 acme_corp__analytics tables: 5 custom: 1 ``` ### Inspecting a Namespace View the tables and columns in a specific namespace: ```bash canvas namespace inspect acme_corp__shared_data --host dev-instance ``` Output separates system tables (managed by the framework) from custom tables (defined by your plugin), and shows column details for custom tables: ```text Namespace: acme_corp__shared_data System tables: namespace_auth ~2 rows schema_version ~2 rows custom_attribute ~150 rows attribute_hub ~3 rows Custom tables: customnote ~25 rows dbid bigint not null title text nullable body text nullable specialty ~8 rows dbid bigint not null name character varying nullable ``` This is useful for verifying that your models were created correctly after installation, or for understanding what's in a namespace before deciding whether to reset or drop it. ### Resetting a Namespace Reset drops your custom tables and truncates the data in system tables, but preserves the namespace itself and its authentication keys. This is the right choice when you want to start fresh with your models while keeping the namespace intact for reinstallation. By default, reset runs in **dry-run mode** and only shows what would happen: ```bash canvas namespace reset acme_corp__shared_data --host dev-instance ``` ```text Namespace: acme_corp__shared_data Custom tables to drop: customnote ~25 rows specialty ~8 rows Data tables to truncate: custom_attribute ~150 rows attribute_hub ~3 rows This is a dry run. To execute, re-run with --execute ``` To actually perform the reset, add `--execute`. You will be prompted to confirm by typing the full namespace name: ```bash canvas namespace reset acme_corp__shared_data --host dev-instance --execute ``` ```text This will reset namespace 'acme_corp__shared_data'. This cannot be undone. Type the full namespace name to confirm: acme_corp__shared_data Namespace 'acme_corp__shared_data' has been reset. Dropped tables: customnote, specialty Truncated tables: custom_attribute, attribute_hub ``` After a reset, reinstall your plugin to recreate the tables with your updated model definitions. ### Dropping a Namespace Drop removes the entire namespace — the schema, all tables, all data, and all authentication keys. Use this when you want to completely remove a namespace and start over, or when you're done with a development namespace and want to clean up. Dry-run mode (default): ```bash canvas namespace drop acme_corp__shared_data --host dev-instance ``` ```text Namespace: acme_corp__shared_data All tables that will be dropped: attribute_hub ~3 rows custom_attribute ~150 rows customnote ~25 rows schema_version ~4 rows namespace_auth ~2 rows specialty ~8 rows This is a dry run. To execute, re-run with --execute ``` To execute: ```bash canvas namespace drop acme_corp__shared_data --host dev-instance --execute ``` After a drop, the next plugin installation with that namespace name will create it from scratch, generating new authentication keys. Any other plugins that were sharing the namespace will need to be reconfigured with the new keys. ## Uninstalling and Reinstalling a Plugin Uninstalling a plugin **deletes its secrets** , including the system-generated `namespace_read_access_key` and `namespace_read_write_access_key`. The namespace schema and its data, however, **survive the uninstall**. This is by design: uninstalling a plugin should never destroy custom data, and a namespace shared by multiple plugins must not be torn down while another plugin is still using it. A plugin deployed with [`canvas deploy`](/sdk/canvas_cli/#canvas-deploy) keeps its access across an uninstall: Canvas Platform holds the namespace's keys and sends them with every install, so the next deploy restores them. The rest of this section applies to plugins installed with the deprecated `canvas install`. Because the namespace still exists, reinstalling the plugin does **not** regenerate the keys — key generation only happens when the namespace is first created. The reinstalled plugin is therefore left without valid access keys and cannot read or write its own data, even though configuring the secrets appeared to succeed. This applies in production as well as during development. To avoid getting stuck: - **Before uninstalling** , copy the namespace keys from the Canvas admin UI into an external secret store such as 1Password. On reinstall, set them back as plugin secrets and the plugin regains access immediately. (To find the keys: open the Canvas admin UI, find the plugin that created the namespace, and read the `namespace_read_access_key` / `namespace_read_write_access_key` secret values.) - **If the keys are already lost** , run `canvas namespace drop --host --execute` to remove the namespace, then reinstall. Installation recreates the namespace and generates fresh keys. Any other plugins that were sharing the namespace must be reconfigured with the new keys. ## When to Reset vs. Drop Scenario| Command ---|--- You renamed or removed a CustomModel and want to clean up the old table| `reset` Test data has accumulated and you want a clean slate| `reset` You changed your namespace name in the manifest| `drop` the old, then reinstall You're done developing and want to remove all traces| `drop` Other plugins share this namespace and you want to preserve their access| `reset` (keys are preserved) You want to regenerate the namespace authentication keys| `drop`, then reinstall You uninstalled a plugin and a reinstall can't access its data (keys were deleted)| `drop`, then reinstall — or restore the saved keys ## See Also - [Quick Start](/sdk/custom-data-quick-start/) \- Get started with custom data in 10 minutes - [CustomModels](/sdk/custom-data-custom-models/) \- Define structured database tables - [Sharing Data](/sdk/custom-data-sharing-data/) \- Share data between plugins - [Testing](/sdk/custom-data-testing/) \- Automated tests for custom data - [Design Considerations](/sdk/custom-data-design-considerations/) \- Choosing the right approach --- # Quick Start Source: https://docs.canvasmedical.com/sdk/custom-data-quick-start/ ## Getting Started To use custom data in your plugin, declare a `custom_data` section in your [`CANVAS_MANIFEST.json`](/sdk/canvas_manifest/#custom-data) 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/custom-data-sharing-data/#custom-data-on-canvas-platform)). ```json { "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 1. Sign in with `canvas login`, then run `canvas init`. These steps assume your organization's plugin prefix is `my_org`. 2. When prompted for a name, enter `Hello Custom Data` 3. `cd hello-custom-data/my_org__hello_custom_data` 4. Open `CANVAS_MANIFEST.json` in your preferred editor. 5. Create a `custom_data` block: ```json "custom_data": { "namespace": "my_org__hello_custom_data", "access": "read_write" } ``` 6. Next, create a `models` directory under the root of your plugin hierarchy, sibling to `CANVAS_MANIFEST.json` and `handlers` 7. Create an `__init__.py` file inside of `models` and open it in your editor. 8. Declare the following classes within the `__init__.py` file: ```python 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() ``` 9. Open `handlers/event_handlers.py` in your editor. Update the imports: ```python from my_org__hello_custom_data.models import CustomNote, NoteTag ``` 10. In the code, replace uses of `Note` with `CustomNote`. These objects behave the same as the SDK model. (See [Extending SDK Models](/sdk/custom-data-extending-sdk-models/) for why proxy models are used.) 11. Add the following lines **after** the `note` reference has been initialized in the code: ```python tag, created = NoteTag.objects.get_or_create( note=note, defaults={"tagged_by": "hello-custom-data"} ) log.info(f"Note tagged by: {tag.tagged_by}") ``` 12. Deploy the plugin to your development instance: `canvas deploy . --instance ` 13. Tail the logs with `canvas logs` 14. 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: ```sql 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](/sdk/custom-data-custom-models) 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](/sdk/custom-data-attribute-hubs/) instead. Replace the `NoteTag` creation in `event_handlers.py` with: ```python 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](/sdk/custom-data-design-considerations/) for help choosing between the two approaches. ## See Also - [Extending SDK Models](/sdk/custom-data-extending-sdk-models/) \- Why proxy models exist and how `related_name` namespacing works - [Transactions](/sdk/custom-data-transactions/) \- All-or-nothing writes with `transaction.atomic()` - [Testing Custom Data](/sdk/custom-data-testing/) \- Testing utilities and examples - [Sharing Data](/sdk/custom-data-sharing-data/) \- Sharing data with other plugins and external services - [Data Models](/sdk/data/) \- Core SDK data models - [Caching API](/sdk/caching) \- Auto-expiring transient data - [Simple API](/sdk/handlers-simple-api/) \- Simple API for sharing data between plugins - [Secrets](/sdk/secrets/) \- Managing API keys and sensitive configuration --- # Sharing Data Source: https://docs.canvasmedical.com/sdk/custom-data-sharing-data/ Plugins can share data in two ways, depending on the relationship between the plugins: Approach| Use Case| Coupling ---|---|--- **Namespace Sharing**| Plugins owned by the same organization that need direct database access| Tight **API Sharing**| Plugins owned by different organizations, or when loose coupling is preferred| Loose ## Namespace Sharing Namespace sharing allows multiple plugins to read from and write to the same database tables. This is ideal for organizations that want to build smaller, focused plugins instead of a single monolithic plugin. A plugin may reside within one namespace only. If it needs to access data from multiple namespaces, then it must do so via API calls. ### When to Use Namespace Sharing - Your organization owns multiple plugins that need to share data - You want to break a large plugin into smaller, maintainable pieces - You need direct database access for performance - You want to avoid the overhead of API calls between plugins ### Namespace Lifecycle For a full overview of how namespaces are created, managed, and cleaned up during development, see [Namespace Lifecycle](/sdk/custom-data-namespace-lifecycle/). ### Custom data on Canvas Platform For a plugin deployed with [`canvas deploy`](/sdk/canvas_cli/#canvas-deploy), Canvas Platform holds each namespace's access keys and sends them to every plugin entitled to them, so no key is copied between plugins: - The first plugin that declares `"access": "read_write"` for a namespace on an instance creates it there and owns it. - Another plugin that declares the same namespace, to read or to write, needs consent from the organization that runs the instance. `canvas deploy` shows each consent request and asks you to answer it; the deployment starts once every request is approved, and an approval stands for later deploys to that instance. - Leave `namespace_read_access_key` and `namespace_read_write_access_key` out of the manifest's `variables`. Canvas Platform supplies them, refuses a manifest that declares them, and refuses `canvas config set` for them. - A namespace that already exists on an instance, created by a plugin installed with `canvas install`, needs its keys recorded with Canvas Platform before a deploy can use it. [Moving to Canvas Platform](/guides/moving-to-canvas-platform/#2-rename-the-plugin-with-the-prefix) covers that step. ### Access keys for plugins installed with `canvas install` (deprecated) > **Warning:** `canvas install` is deprecated, and support ends on December 14, 2026. The rest of this section describes how plugins installed straight onto an instance share a namespace. Plugins deployed with `canvas deploy` share a namespace as described in Custom data on Canvas Platform. #### Discovering Access Keys After the namespace is created, you can find the generated keys in the **Canvas admin UI** : 1. Navigate to **Settings → Plugins** 2. Find the plugin that created the namespace 3. Click to view plugin details 4. The `namespace_read_access_key` and `namespace_read_write_access_key` appear in the **Secrets** section Share these keys securely with developers of other plugins that need access: - Share `namespace_read_access_key` with plugins that only need to read data - Share `namespace_read_write_access_key` with plugins that need to modify data > **Important:** Store these keys in a secure location outside of Canvas, such as 1Password. Removing a key from the manifest's `secrets` array does **not** delete the stored value — it is preserved. However, **uninstalling the plugin deletes its secrets** , including the namespace keys. Because the namespace itself survives an uninstall, a later reinstall will not regenerate the keys, so a copy kept outside Canvas is the only way to restore access. See [Uninstalling and Reinstalling a Plugin](/sdk/custom-data-namespace-lifecycle/#uninstalling-and-reinstalling-a-plugin). #### Pre-Supplying Keys at Creation By default, Canvas auto-generates the two access keys the first time a `read_write` plugin creates a namespace. You can also **supply both keys yourself** at that first install — pass them through the same `--secret` mechanism used for joins: ```bash canvas install my_plugin \ --host demo.canvasmedical.com \ --secret namespace_read_access_key= \ --secret namespace_read_write_access_key= ``` When both keys are present at the install that creates the namespace, those plaintext values are what get hashed into the namespace's authentication table — Canvas does **not** generate fresh UUIDs. Rules: - Supply **both** keys for your values to take effect. If you provide only one — or leave either value empty — Canvas silently ignores the supplied keys and auto-generates both instead. The install still succeeds; it does not fail. - Format is not enforced, but UUID4s are conventional. - This only applies to the install that **creates** the namespace. Subsequent joins validate against whatever was written at creation time. - If you supply neither key, behavior is unchanged: Canvas generates both for you. When to use this: - You want the keys to be known and persisted outside Canvas before the namespace exists (for example, a deployment system that needs to seed sibling plugins with the same key without round-tripping through the Admin UI). - You're restoring access to a previously-dropped namespace and want to reuse known key values. - You want deterministic key values across test runs in CI. #### Configuring Plugin Access Each plugin that joins a namespace must: 1. **Declare the namespace** in `CANVAS_MANIFEST.json` 2. **Include the access key name** in the manifest's `variables` array 3. **Provide the access key** during installation ```json { "variables": [ {"name": "namespace_read_write_access_key", "sensitive": false} ], "custom_data": { "namespace": "acme_corp__shared_data", "access": "read_write" } } ``` > Namespace access keys are declared with `"sensitive": false` so the value remains readable in the Admin UI's Secrets inline — that's the surface developers use to copy a key into a sibling plugin that joins the same namespace. **Installing with the Canvas CLI:** Provide the access key using the `--secret` flag: ```bash canvas install my_plugin \ --host demo.canvasmedical.com \ --secret namespace_read_write_access_key=3b35fad9-6462-4e83-83f5-c0e4bde49b71 ``` **Alternative: Setting secrets via Admin UI:** If you've already installed the plugin without the secret: 1. Go to **Settings → Plugins → Your Plugin → Secrets** 2. Set the `namespace_read_access_key` or `namespace_read_write_access_key` value 3. **Reinstall the plugin** to pick up the secret ### Manifest Configuration ```json { "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" } } ``` A plugin installed with `canvas install` also declares the access key it is given in `variables`, as shown in Configuring Plugin Access. **Namespace naming requirements:** - Must contain `__` (double underscore) to separate organization from name - Cannot use reserved PostgreSQL names (`public`, `pg_catalog`, etc.) - Organizations and names must start with a letter **Access levels:** - `read` \- Can only read data from the namespace - `read_write` \- Can read and write data, and create custom tables ### Permissions and Restrictions Permission| `read`| `read_write` ---|---|--- Query AttributeHubs| ✅| ✅ Query CustomModels| ✅| ✅ Create/update/delete AttributeHubs| ❌| ✅ Create/update/delete CustomModel records| ❌| ✅ Create/update custom database tables| ❌| ✅ ### Example: Sharing AttributeHubs AttributeHubs store standalone key-value data not attached to Canvas models. **Plugin A (write access) - Creates configuration hub:** ```python # CANVAS_MANIFEST.json: "access": "read_write" from canvas_sdk.v1.data import AttributeHub # Create or retrieve a configuration hub config, created = AttributeHub.objects.get_or_create(type="clinic_config", id="main") config.set_attribute("max_daily_appointments", 50) config.set_attribute("appointment_duration_minutes", 30) config.set_attribute("accepting_new_patients", True) ``` **Plugin B (read access) - Reads configuration:** ```python # CANVAS_MANIFEST.json: "access": "read" from canvas_sdk.v1.data import AttributeHub config = AttributeHub.objects.with_only( attribute_names=["max_daily_appointments", "appointment_duration_minutes"] ).get(type="clinic_config", id="main") max_appointments = config.get_attribute("max_daily_appointments") # 50 duration = config.get_attribute("appointment_duration_minutes") # 30 ``` ### Example: Sharing CustomModels CustomModels allow you to define your own database tables with full ORM support. **Important:** If multiple plugins need to share the same custom tables, each plugin must declare identical model definitions. The `read_write` plugin creates the tables; `read` plugins can query but not modify them. **Shared model definition (must be identical in both plugins):** ```python # models/specialty.py from django.db import models from canvas_sdk.v1.data.base import CustomModel class Specialty(CustomModel): """A medical specialty that can be assigned to staff members.""" name = models.CharField(max_length=100, unique=True) description = models.TextField(blank=True) requires_referral = models.BooleanField(default=False) class Meta: indexes = [ models.Index(fields=['name']), ] ``` **Plugin A (write access) - Creates and manages specialties:** ```python # CANVAS_MANIFEST.json: "access": "read_write" from .models.specialty import Specialty # Create specialties cardiology = Specialty( name="Cardiology", description="Heart and cardiovascular system", requires_referral=True ) cardiology.save() dermatology = Specialty( name="Dermatology", description="Skin conditions", requires_referral=False ) dermatology.save() ``` **Plugin B (read access) - Queries specialties:** ```python # CANVAS_MANIFEST.json: "access": "read" from .models.specialty import Specialty # Query specialties (read operations work) referral_specialties = Specialty.objects.filter(requires_referral=True) for specialty in referral_specialties: print(f"{specialty.name}: {specialty.description}") # Write operations raise NamespaceWriteDenied specialty = Specialty.objects.first() specialty.description = "Updated" specialty.save() # Raises NamespaceWriteDenied! ``` ### Error Handling When a plugin with `read` access attempts a write operation, a `NamespaceWriteDenied` exception is raised: ```python from canvas_sdk.v1.data.base import NamespaceWriteDenied try: hub.set_attribute("key", "value") except NamespaceWriteDenied as e: # "Write operation denied: namespace 'acme_corp__shared_data' is read-only. # Plugin must declare 'read_write' access to perform write operations." log.error(f"Cannot write to shared namespace: {e}") ``` ### Troubleshooting **"NamespaceAccessError: secret 'namespace_read_access_key' is not configured"** - Add the secret name to the `secrets` array in your manifest - Ensure the secret has a value set in the Canvas UI **"NamespaceAccessError: the key value is not a valid access key"** - Verify you're using the correct key from the namespace owner - Check that the key hasn't been regenerated **"NamespaceAccessError: requests 'read_write' access but key only grants 'read'"** - You're using `namespace_read_access_key` but declared `"access": "read_write"` - Either change to `namespace_read_write_access_key` or change access to `"read"` **"NamespaceWriteDenied: namespace is read-only"** - Your plugin has `"access": "read"` but is attempting a write operation - Change to `"access": "read_write"` and use `namespace_read_write_access_key` ## API Sharing API sharing is the recommended approach when: - Plugins are owned by different organizations - You want loose coupling between plugins - You need fine-grained control over what data is exposed - You want to version your data interface independently ### Example: Exposing Provider Profile Data ```python from canvas_sdk.handlers.simple_api import SimpleAPI, APIKeyCredentials, api from canvas_sdk.effects.simple_api import JSONResponse from canvas_sdk.v1.data import Staff, ModelExtension from canvas_sdk.v1.data.base import CustomModel from django.db.models import BooleanField, DO_NOTHING, OneToOneField, TextField class CustomStaff(Staff, ModelExtension): pass class StaffProfile(CustomModel): staff = OneToOneField( CustomStaff, to_field="dbid", on_delete=DO_NOTHING, related_name="profile" ) specialty = TextField() accepting_patients = BooleanField(default=True) class ProfileAPI(SimpleAPI): """API to share staff profile data with authorized plugins.""" PREFIX = "/staff-profiles" def authenticate(self, credentials: APIKeyCredentials) -> bool: """Validate API key from requesting plugin.""" from hmac import compare_digest provided_key = credentials.key expected_key = self.secrets["profile_api_key"] return compare_digest(provided_key.encode(), expected_key.encode()) @api.get("/") def get_profile(self): """Return staff profile data.""" staff_id = self.request.path_params["staff_id"] staff = CustomStaff.objects.select_related("profile").get(id=staff_id) # Explicitly choose what data to expose profile = { "staff_id": staff.id, "first_name": staff.first_name, "last_name": staff.last_name, "specialty": staff.profile.specialty, "accepting_patients": staff.profile.accepting_patients } return [JSONResponse(profile)] ``` ### Consuming Shared Data from Another Plugin ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.simple_api import Response, JSONResponse from canvas_sdk.handlers.simple_api import SimpleAPI, api from canvas_sdk.utils import Http class MyAPI(SimpleAPI): PREFIX = "/retrieve" @api.get("/profile_for_staff/") def get_single_profile_via_api(self) -> list[Response | Effect]: staff_id = self.request.path_params["staff_id"] canvas_host = f"{self.environment['CUSTOMER_IDENTIFIER']}.canvasmedical.com" token = self.secrets["profile_api_token"] other_plugin_api = f"https://{canvas_host}/plugin-io/api/other_plugin/staff-profiles/{staff_id}" http = Http() response = http.get(other_plugin_api, headers={"Authorization": token}) return [JSONResponse(response.json())] ``` ### API Sharing Best Practices 1. **Explicit Authorization** \- Always require authentication for APIs that expose plugin data 2. **Minimal Exposure** \- Only expose the specific data fields that are necessary 3. **Validate Requests** \- Check permissions and validate that the requester should have access 4. **Document APIs** \- Provide clear documentation for plugins that will consume your API 5. **Version APIs** \- Use versioning (e.g., `/v1/profiles`) to allow API evolution 6. **Audit Access** \- Log API access for security and debugging purposes 7. **Rate Limiting** \- Consider implementing rate limits to prevent abuse ### Security Considerations - **Never bypass plugin isolation** by attempting to access another plugin's database schema directly - **Use API keys or tokens** stored in secrets, never hardcoded in plugin code - **Implement proper error handling** that doesn't leak sensitive information - **Consider PHI implications** when exposing patient-related data via APIs - **Follow least privilege** principle - grant minimum necessary access ## Choosing Between Namespace and API Sharing Factor| Namespace Sharing| API Sharing ---|---|--- **Ownership**| Same organization| Different organizations **Coupling**| Tight| Loose **Performance**| Direct DB access| HTTP overhead **Schema Evolution**| Coordinated updates| Independent versioning **Access Control**| Binary (read/read_write)| Fine-grained **Setup Complexity**| Lower| Higher ## See Also - [Custom Data Overview](/sdk/custom-data/) \- Introduction to custom data storage - [Namespace Lifecycle](/sdk/custom-data-namespace-lifecycle/) \- Managing namespaces during development - [AttributeHubs](/sdk/custom-data-attribute-hubs/) \- Standalone key-value storage - [CustomModels](/sdk/custom-data-custom-models/) \- Django models for structured data - [Testing Utils](/sdk/testing-utils/) \- Factories for testing custom data - [Caching API](/sdk/caching) \- Auto-expiring transient data - [Simple API](/sdk/handlers-simple-api-http) \- HTTP API handlers - [Secrets](/sdk/secrets/) \- Managing API keys and sensitive configuration --- # Testing Custom Data Source: https://docs.canvasmedical.com/sdk/custom-data-testing/ The Canvas SDK provides comprehensive testing utilities for custom data. Tests run within database transactions that automatically roll back, ensuring isolation between test cases. ## Test Setup Install the test utilities extra to enable pytest-based testing: ```bash uv add "canvas[test-utils]" ``` Run your tests with: ```bash uv run pytest ``` Each test runs inside a transaction and automatically rolls back at the end, providing clean isolation without manual cleanup. See [Testing Utilities](/sdk/testing-utils/) for complete setup instructions. ## Creating Factories for Extended Models Define factories for extended models by extending the base SDK factories: ```python import factory from canvas_sdk.test_utils.factories import StaffFactory, PatientFactory from staff_plus.models import CustomStaff, CustomPatient class CustomStaffFactory(StaffFactory, factory.django.DjangoModelFactory[CustomStaff]): """Factory for creating CustomStaff instances.""" class Meta: model = CustomStaff class CustomPatientFactory(PatientFactory, factory.django.DjangoModelFactory[CustomPatient]): """Factory for creating CustomPatient instances.""" class Meta: model = CustomPatient ``` ## Creating Factories for Custom Models Define factories for your custom models with appropriate field values: ```python import factory from my_plugin.models import Specialty, StaffSpecialty from my_plugin.models import Biography class SpecialtyFactory(factory.django.DjangoModelFactory): """Factory for creating Specialty instances.""" class Meta: model = Specialty django_get_or_create = ("name",) # Avoid duplicate specialties name = factory.Faker("random_element", elements=[ "Cardiology", "Dermatology", "Neurology", "Orthopedics", "Pediatrics", "Psychiatry", "Radiology", "Surgery" ]) class BiographyFactory(factory.django.DjangoModelFactory): """Factory for creating Biography instances.""" class Meta: model = Biography staff = factory.SubFactory(CustomStaffFactory) biography = factory.Faker("paragraph", nb_sentences=5) language = factory.Faker("language_name") practicing_since = factory.Faker("year") class StaffSpecialtyFactory(factory.django.DjangoModelFactory): """Factory for many-to-many relationship.""" class Meta: model = StaffSpecialty staff = factory.SubFactory(CustomStaffFactory) specialty = factory.SubFactory(SpecialtyFactory) ``` ## Testing AttributeHub Test that AttributeHub stores and retrieves data correctly: ```python from datetime import datetime import factory from canvas_sdk.test_utils.factories import StaffFactory from canvas_sdk.v1.data import AttributeHub, Staff, ModelExtension class CustomStaff(Staff, ModelExtension): pass class CustomStaffFactory(StaffFactory, factory.django.DjangoModelFactory[CustomStaff]): class Meta: model = CustomStaff def test_attribute_hub_creation(): """Test creating and using AttributeHub.""" # Create hub hub = AttributeHub.objects.create( type="staff_profile", id="staff_123" ) # Set attributes hub.set_attribute("last_sync", datetime.now()) hub.set_attribute("external_id", "ext_456") # Verify persistence hub_from_db = AttributeHub.objects.get(dbid=hub.dbid) assert hub_from_db.get_attribute("external_id") == "ext_456" def test_attribute_hub_get_or_create(): """Test get_or_create pattern with AttributeHub.""" staff = CustomStaffFactory.create() # First call creates hub1, created1 = AttributeHub.objects.get_or_create( type="staff_sync", id=f"staff:{staff.id}" ) assert created1 is True hub1.set_attribute("data", {"key": "value"}) # Second call retrieves existing hub2, created2 = AttributeHub.objects.get_or_create( type="staff_sync", id=f"staff:{staff.id}" ) assert created2 is False assert hub1.dbid == hub2.dbid assert hub2.get_attribute("data") == {"key": "value"} def test_attribute_hub_json_storage(): """Test storing complex JSON in AttributeHub.""" hub = AttributeHub.objects.create( type="profile", id="test_123" ) profile_data = { "biography": "Experienced physician", "specialties": ["Cardiology", "Internal Medicine"], "languages": ["English", "Spanish"], "practicing_since": 2005, "accepting_patients": False } hub.set_attribute("profile", profile_data) hub_from_db = AttributeHub.objects.get(dbid=hub.dbid) retrieved = hub_from_db.get_attribute("profile") assert retrieved == profile_data assert retrieved["biography"] == "Experienced physician" assert len(retrieved["specialties"]) == 2 ``` ## Testing Custom Models Test custom model creation, relationships, and queries: ```python import factory from datetime import datetime from django.db.models import ( ForeignKey, ManyToManyField, OneToOneField, TextField, IntegerField, DateTimeField, Index, DO_NOTHING ) from canvas_sdk.test_utils.factories import StaffFactory from canvas_sdk.v1.data import Staff, ModelExtension from canvas_sdk.v1.data.base import CustomModel class CustomStaff(Staff, ModelExtension): pass class CustomStaffFactory(StaffFactory, factory.django.DjangoModelFactory[CustomStaff]): class Meta: model = CustomStaff class Specialty(CustomModel): class Meta: indexes = [ Index(fields=["name"]), ] name = TextField() staff_members = ManyToManyField( "CustomStaff", through="StaffSpecialty", related_name="specialties", ) class Biography(CustomModel): staff = OneToOneField( CustomStaff, to_field="dbid", on_delete=DO_NOTHING, related_name="biography" ) biography = TextField() language = TextField() practicing_since = IntegerField() class Language(CustomModel): staff = ForeignKey( CustomStaff, to_field="dbid", on_delete=DO_NOTHING, related_name="languages" ) name = TextField() code = TextField() created = DateTimeField(default=datetime.now) class StaffSpecialty(CustomModel): staff = ForeignKey( CustomStaff, to_field="dbid", on_delete=DO_NOTHING, related_name="staff_specialties" ) specialty = ForeignKey( Specialty, to_field="dbid", on_delete=DO_NOTHING, related_name="staff_specialties" ) def test_custom_model_creation(): """Test creating custom models.""" specialty = Specialty.objects.create(name="Cardiology") assert specialty.dbid is not None assert specialty.name == "Cardiology" # Verify persistence specialty_from_db = Specialty.objects.get(dbid=specialty.dbid) assert specialty_from_db.name == "Cardiology" def test_one_to_one_relationship(): """Test one-to-one relationships.""" staff = CustomStaffFactory.create() # Create related biography biography = Biography.objects.create( staff=staff, biography="Experienced cardiologist", language="English", practicing_since=2005 ) # Access from biography to staff assert biography.staff.id == staff.id # Access from staff to biography (reverse relation) staff_from_db = CustomStaff.objects.get(id=staff.id) assert staff_from_db.biography.biography == "Experienced cardiologist" assert staff_from_db.biography.practicing_since == 2005 def test_one_to_many_relationship(): """Test one-to-many relationships.""" staff = CustomStaffFactory.create() # Create multiple related languages Language.objects.create(staff=staff, name="English", code="en") Language.objects.create(staff=staff, name="Spanish", code="es") Language.objects.create(staff=staff, name="French", code="fr") # Access all languages via reverse relation languages = staff.languages.all() assert languages.count() == 3 language_names = [lang.name for lang in languages] assert "English" in language_names assert "Spanish" in language_names assert "French" in language_names def test_many_to_many_relationship(): """Test many-to-many relationships via junction table.""" staff = CustomStaffFactory.create() cardiology = Specialty.objects.create(name="Cardiology") internal_med = Specialty.objects.create(name="Internal Medicine") # Create associations StaffSpecialty.objects.create(staff=staff, specialty=cardiology) StaffSpecialty.objects.create(staff=staff, specialty=internal_med) # Query specialties for staff staff_specialties = staff.staff_specialties.all() assert staff_specialties.count() == 2 specialty_names = [ss.specialty.name for ss in staff_specialties] assert "Cardiology" in specialty_names assert "Internal Medicine" in specialty_names # Query staff by specialty staff_ids = ( StaffSpecialty.objects .filter(specialty__name="Cardiology") .values_list("staff_id", flat=True) ) assert staff.dbid in staff_ids def test_many_to_many_query_filtering(): """Test querying across many-to-many relationships.""" staff1 = CustomStaffFactory.create() staff2 = CustomStaffFactory.create() cardiology = Specialty.objects.create(name="Cardiology") neurology = Specialty.objects.create(name="Neurology") StaffSpecialty.objects.create(staff=staff1, specialty=cardiology) StaffSpecialty.objects.create(staff=staff2, specialty=neurology) StaffSpecialty.objects.create(staff=staff2, specialty=cardiology) # Find all staff with cardiology cardiology_staff_ids = ( StaffSpecialty.objects .filter(specialty__name="Cardiology") .values_list("staff_id", flat=True) ) assert staff1.dbid in cardiology_staff_ids assert staff2.dbid in cardiology_staff_ids # Find staff with multiple specialties multi_specialty_ids = ( StaffSpecialty.objects .filter(specialty__name__in=["Cardiology", "Neurology"]) .values_list("staff_id", flat=True) .distinct() ) assert len(multi_specialty_ids) == 2 def test_many_to_many_through_field(): """Test direct M2M traversal via ManyToManyField(through=...).""" staff = CustomStaffFactory.create() cardiology = Specialty.objects.create(name="Cardiology") internal_med = Specialty.objects.create(name="Internal Medicine") StaffSpecialty.objects.create(staff=staff, specialty=cardiology) StaffSpecialty.objects.create(staff=staff, specialty=internal_med) # Direct M2M traversal — Specialty → staff assert staff in cardiology.staff_members.all() # Reverse M2M traversal — staff → specialties specialty_names = [s.name for s in staff.specialties.all()] assert "Cardiology" in specialty_names assert "Internal Medicine" in specialty_names ``` ## Testing with Factories Use factories to simplify test data creation: ```python import factory from django.db.models import OneToOneField, TextField, IntegerField, DO_NOTHING from canvas_sdk.test_utils.factories import StaffFactory from canvas_sdk.v1.data import Staff, ModelExtension from canvas_sdk.v1.data.base import CustomModel class CustomStaff(Staff, ModelExtension): pass class CustomStaffFactory(StaffFactory, factory.django.DjangoModelFactory[CustomStaff]): class Meta: model = CustomStaff class Biography(CustomModel): staff = OneToOneField( CustomStaff, to_field="dbid", on_delete=DO_NOTHING, related_name="biography" ) biography = TextField() language = TextField() practicing_since = IntegerField() class BiographyFactory(factory.django.DjangoModelFactory): class Meta: model = Biography staff = factory.SubFactory(CustomStaffFactory) biography = factory.Faker("paragraph", nb_sentences=5) language = factory.Faker("language_name") practicing_since = factory.Faker("year") def test_with_factories(): """Test using factories for quick data setup.""" # Create staff with biography using factories biography = BiographyFactory.create() assert biography.staff is not None assert biography.biography is not None assert biography.practicing_since is not None # Factory automatically created the related staff staff = biography.staff assert staff.first_name is not None class StaffSpecialty(CustomModel): staff = ForeignKey( CustomStaff, to_field="dbid", on_delete=DO_NOTHING, related_name="staff_specialties" ) specialty = ForeignKey( Specialty, to_field="dbid", on_delete=DO_NOTHING, related_name="staff_specialties" ) class StaffSpecialtyFactory(factory.django.DjangoModelFactory): class Meta: model = StaffSpecialty staff = factory.SubFactory(CustomStaffFactory) specialty = factory.SubFactory(SpecialtyFactory) def test_many_to_many_with_factories(): """Test many-to-many relationships with factories.""" # Create staff-specialty associations ss1 = StaffSpecialtyFactory.create() ss2 = StaffSpecialtyFactory.create(staff=ss1.staff) # Same staff, different specialty # Verify relationships assert ss1.staff.staff_specialties.count() == 2 ``` ## Testing Queries and Prefetching Test that prefetching and query optimization work correctly: ```python import factory from django.db.models import ( ForeignKey, OneToOneField, TextField, IntegerField, Index, DO_NOTHING, Count ) from canvas_sdk.test_utils.factories import StaffFactory from canvas_sdk.v1.data import AttributeHub, Staff, ModelExtension from canvas_sdk.v1.data.base import CustomModel class CustomStaff(Staff, ModelExtension): pass class CustomStaffFactory(StaffFactory, factory.django.DjangoModelFactory[CustomStaff]): class Meta: model = CustomStaff class Biography(CustomModel): staff = OneToOneField( CustomStaff, to_field="dbid", on_delete=DO_NOTHING, related_name="biography" ) biography = TextField() practicing_since = IntegerField() class Specialty(CustomModel): class Meta: indexes = [ Index(fields=["name"]), ] name = TextField() class StaffSpecialty(CustomModel): staff = ForeignKey( CustomStaff, to_field="dbid", on_delete=DO_NOTHING, related_name="staff_specialties" ) specialty = ForeignKey( Specialty, to_field="dbid", on_delete=DO_NOTHING, related_name="staff_specialties" ) class BiographyFactory(factory.django.DjangoModelFactory): class Meta: model = Biography staff = factory.SubFactory(CustomStaffFactory) biography = factory.Faker("paragraph") practicing_since = factory.Faker("year") class SpecialtyFactory(factory.django.DjangoModelFactory): class Meta: model = Specialty name = factory.Faker("word") class StaffSpecialtyFactory(factory.django.DjangoModelFactory): class Meta: model = StaffSpecialty staff = factory.SubFactory(CustomStaffFactory) specialty = factory.SubFactory(SpecialtyFactory) def test_attribute_hub_prefetch(): """Test prefetching AttributeHub attributes.""" hub1 = AttributeHub.objects.create(type="profile", id="staff_1") hub2 = AttributeHub.objects.create(type="profile", id="staff_2") hub1.set_attribute("specialty", "Cardiology") hub2.set_attribute("specialty", "Neurology") # Query with automatic prefetch (default behavior) hubs = AttributeHub.objects.filter(type="profile") # Access attributes without additional queries for hub in hubs: specialty = hub.get_attribute("specialty") assert specialty in ["Cardiology", "Neurology"] def test_attribute_hub_with_only(): """Test selective attribute prefetching on AttributeHub.""" hub = AttributeHub.objects.create(type="profile", id="staff_1") hub.set_attributes({ "specialty": "Cardiology", "years_experience": 15, "accepting_patients": True }) # Prefetch only specific attributes hub_from_db = ( AttributeHub.objects .with_only(["specialty", "accepting_patients"]) .get(dbid=hub.dbid) ) # Prefetched attributes accessible assert hub_from_db.get_attribute("specialty") == "Cardiology" assert hub_from_db.get_attribute("accepting_patients") is True def test_relationship_prefetch(): """Test prefetching related models.""" staff1 = CustomStaffFactory.create() staff2 = CustomStaffFactory.create() BiographyFactory.create(staff=staff1) BiographyFactory.create(staff=staff2) cardiology = SpecialtyFactory.create(name="Cardiology") StaffSpecialtyFactory.create(staff=staff1, specialty=cardiology) StaffSpecialtyFactory.create(staff=staff2, specialty=cardiology) # Prefetch all relationships all_staff = ( CustomStaff.objects .prefetch_related("biography") .prefetch_related("staff_specialties__specialty") .all() ) # Access without additional queries for staff in all_staff: bio = staff.biography.biography specialties = [ss.specialty.name for ss in staff.staff_specialties.all()] assert bio is not None assert len(specialties) > 0 def test_select_related(): """Test select_related for FK and OneToOne joins.""" staff = CustomStaffFactory.create() BiographyFactory.create(staff=staff) StaffSpecialtyFactory.create(staff=staff) # select_related eagerly loads FK/O2O relations in a single query specialty_assoc = ( StaffSpecialty.objects .select_related("staff", "specialty") .filter(staff=staff) .first() ) assert specialty_assoc.staff.first_name is not None assert specialty_assoc.specialty.name is not None ``` ## Testing Data Integrity Test data validation, constraints, and cascade behavior: ```python from datetime import datetime import factory import pytest from django.db import IntegrityError from django.db.models import ( CASCADE, DateTimeField, ForeignKey, TextField, Index, UniqueConstraint, DO_NOTHING ) from canvas_sdk.test_utils.factories import StaffFactory from canvas_sdk.v1.data import AttributeHub, Staff, ModelExtension from canvas_sdk.v1.data.base import CustomModel class CustomStaff(Staff, ModelExtension): pass class CustomStaffFactory(StaffFactory, factory.django.DjangoModelFactory[CustomStaff]): class Meta: model = CustomStaff class Specialty(CustomModel): class Meta: indexes = [ Index(fields=["name"]), ] name = TextField() class SpecialtyFactory(factory.django.DjangoModelFactory): class Meta: model = Specialty name = factory.Faker("word") class StaffSpecialty(CustomModel): staff = ForeignKey( CustomStaff, to_field="dbid", on_delete=DO_NOTHING, related_name="staff_specialties" ) specialty = ForeignKey( Specialty, to_field="dbid", on_delete=DO_NOTHING, related_name="staff_specialties" ) class StaffSpecialtyFactory(factory.django.DjangoModelFactory): class Meta: model = StaffSpecialty staff = factory.SubFactory(CustomStaffFactory) specialty = factory.SubFactory(SpecialtyFactory) class Team(CustomModel): class Meta: constraints = [ UniqueConstraint(fields=["name"], name="unique_team_name"), ] name = TextField() class TeamMember(CustomModel): class Meta: constraints = [ UniqueConstraint( fields=["team", "staff"], name="unique_team_staff", ), ] team = ForeignKey(Team, to_field="dbid", on_delete=CASCADE, related_name="members") staff = ForeignKey( CustomStaff, to_field="dbid", on_delete=DO_NOTHING, related_name="team_memberships" ) joined_at = DateTimeField() class TeamFactory(factory.django.DjangoModelFactory): class Meta: model = Team name = factory.Sequence(lambda n: f"Team {n + 1}") class TeamMemberFactory(factory.django.DjangoModelFactory): class Meta: model = TeamMember team = factory.SubFactory(TeamFactory) staff = factory.SubFactory(CustomStaffFactory) joined_at = factory.LazyFunction(datetime.now) def test_manual_cleanup_on_delete(): """Test manual cleanup for DO_NOTHING foreign keys. ForeignKeys to SDK models (Staff, Patient, etc.) must use DO_NOTHING because those tables are managed externally. Related records must be deleted manually before deleting the parent. """ staff = CustomStaffFactory.create() specialty = SpecialtyFactory.create() ss = StaffSpecialtyFactory.create(staff=staff, specialty=specialty) # With DO_NOTHING, you must clean up related records manually specialty_id = specialty.dbid StaffSpecialty.objects.filter(specialty_id=specialty_id).delete() specialty.delete() # Verify both are gone assert not StaffSpecialty.objects.filter(specialty_id=specialty_id).exists() assert not Specialty.objects.filter(dbid=specialty_id).exists() def test_cascade_delete(): """Test CASCADE deletion between custom models. ForeignKeys between your own CustomModels can use CASCADE to automatically delete related records. """ team = TeamFactory.create() TeamMemberFactory.create(team=team) TeamMemberFactory.create(team=team) assert TeamMember.objects.filter(team=team).count() == 2 # Deleting the team cascades to members team.delete() assert not TeamMember.objects.filter(team=team).exists() def test_unique_constraint_violation(): """Test that UniqueConstraint prevents duplicate records.""" team = TeamFactory.create() staff = CustomStaffFactory.create() TeamMember.objects.create(team=team, staff=staff, joined_at=datetime.now()) # Same team + staff violates the UniqueConstraint with pytest.raises(IntegrityError): TeamMember.objects.create(team=team, staff=staff, joined_at=datetime.now()) def test_attribute_hub_upsert(): """Test that set_attribute updates existing values rather than creating duplicates.""" hub = AttributeHub.objects.create(type="test", id="upsert_test") # Set attribute hub.set_attribute("field", "value1") # Setting same attribute name should update, not create duplicate hub.set_attribute("field", "value2") # Verify only the updated value exists hub_from_db = AttributeHub.objects.get(dbid=hub.dbid) assert hub_from_db.get_attribute("field") == "value2" def test_transaction_rollback(): """Verify that tests automatically roll back.""" # This test demonstrates automatic rollback # Data created here won't exist in subsequent tests staff = CustomStaffFactory.create() staff_id = staff.id specialty = SpecialtyFactory.create(name="Test Specialty") # After this test, these objects won't exist in other tests # due to automatic transaction rollback assert staff_id is not None assert specialty.name == "Test Specialty" ``` ## Testing proxy_field The `proxy_field` descriptor lets a `ModelExtension` proxy transparently return another proxy class from a ForeignKey lookup, so you can access custom methods on related objects: ```python from canvas_sdk.v1.data import Note, Patient, Staff, ModelExtension from canvas_sdk.v1.data.base import proxy_field from canvas_sdk.test_utils.factories import NoteFactory class CustomPatient(Patient, ModelExtension): @property def display_name(self) -> str: return f"{self.first_name} {self.last_name}" class CustomNote(Note, ModelExtension): # Without proxy_field, accessing note.patient returns a plain Patient. # With proxy_field, it returns a CustomPatient instead. patient = proxy_field(CustomPatient) def test_proxy_field_returns_proxy_class(): """proxy_field swaps __class__ so the returned object is CustomPatient.""" note = NoteFactory.create() custom_note = CustomNote.objects.select_related("patient").get(dbid=note.dbid) # The patient is a CustomPatient, not a plain Patient assert type(custom_note.patient) is CustomPatient assert custom_note.patient.display_name == ( f"{note.patient.first_name} {note.patient.last_name}" ) def test_proxy_field_handles_null(): """proxy_field returns None when the FK is null.""" note = NoteFactory.create(patient=None) custom_note = CustomNote.objects.get(dbid=note.dbid) assert custom_note.patient is None ``` ## Testing Best Practices 1. **Use factories** for consistent test data generation 2. **Test isolation** \- Each test should be independent and not rely on data from other tests 3. **Test both directions** of relationships (forward and reverse) 4. **Verify persistence** by reloading objects from the database 5. **Test edge cases** like None values, empty lists, and missing relationships 6. **Use descriptive test names** that explain what is being tested 7. **Test query optimization** to ensure prefetching works as expected 8. **Verify constraints** like uniqueness behavior 9. **Choose the right`on_delete`** \- ForeignKeys to SDK models (Staff, Patient, etc.) must use `DO_NOTHING` and related records must be deleted manually. ForeignKeys between your own CustomModels can use `CASCADE` for automatic cleanup ## See Also - [Custom Data Overview](/sdk/custom-data/) \- Introduction to custom data storage - [CustomModels](/sdk/custom-data-custom-models/) \- Django models for structured data - [AttributeHubs](/sdk/custom-data-attribute-hubs/) \- Standalone key-value storage - [Sharing Data](/sdk/custom-data-sharing-data/) \- Sharing data among plugins - [Caching API](/sdk/caching) \- Auto-expiring transient data --- # Transactions Source: https://docs.canvasmedical.com/sdk/custom-data-transactions/ ## Overview By default, each ORM operation in a plugin (`.save()`, `.create()`, `.update()`, `.delete()`) is committed to the database immediately. There is no automatic transaction wrapping your handler or protocol — if you perform three writes and the third one fails, the first two are already committed. When you need multiple operations to succeed or fail together, use `transaction.atomic()`. * * * ## Using `transaction.atomic()` Wrap related operations in an `atomic()` block to ensure all-or-nothing behavior: ```python from django.db.transaction import atomic with atomic(): # All operations inside this block are part of a single transaction. # If any operation raises an exception, everything is rolled back. specialty, _ = Specialty.objects.get_or_create(name="Cardiology") StaffSpecialty.objects.filter(staff=staff).delete() StaffSpecialty.objects.bulk_create([ StaffSpecialty(staff=staff, specialty=specialty) ]) Biography.objects.create(staff=staff, biography="...") ``` If an exception occurs anywhere inside the block, all changes are rolled back — the database is left as it was before the block started. * * * ## When to Use Transactions Use `transaction.atomic()` when your handler performs **multiple related writes** that should not be partially applied: - **Replacing associations** — deleting existing records and creating new ones (e.g., replacing a staff member's specialties). Without a transaction, a failure after the delete leaves the staff member with no specialties. - **Creating a parent and its children** — e.g., creating a `Biography` and several `Language` records in one request. A partial failure could leave orphaned or incomplete data. - **Coordinated updates** — updating multiple models that must stay consistent with each other. You do **not** need a transaction for: - A single `.create()`, `.save()`, or `.update()` call — these are already atomic on their own. - Read-only operations — `SELECT` queries don't modify data. * * * ## Example: Multi-Model Upsert This example accepts a JSON payload and upserts a staff profile spanning multiple CustomModels. The `atomic()` block ensures that either the entire profile is saved or nothing is: ```python from django.db.transaction import atomic from canvas_sdk.effects.simple_api import JSONResponse from canvas_sdk.handlers.simple_api import SimpleAPI, api class ProfileAPI(SimpleAPI): PREFIX = "/profile" @api.post("/v2/") def post_profile(self): with atomic(): staff_id = self.request.path_params["staff_id"] json_body = self.request.json() staff = CustomStaff.objects.get(id=staff_id) # Upsert languages for name in json_body.get("languages", []): Language.objects.get_or_create(name=name, staff=staff) # Replace specialty associations specialties = [] for name in json_body.get("specialties", []): specialty, _ = Specialty.objects.get_or_create(name=name) specialties.append(specialty) StaffSpecialty.objects.filter(staff=staff).delete() StaffSpecialty.objects.bulk_create([ StaffSpecialty(staff=staff, specialty=s) for s in specialties ]) # Upsert biography biography_text = json_body.get("biography") Biography.objects.update_or_create( staff=staff, defaults={ "biography": biography_text, "practicing_since": json_body.get("practicing_since"), "is_accepting_patients": json_body.get("accepting_patients"), }, ) return [JSONResponse({"status": "ok"})] ``` If any operation inside the `atomic()` block raises an exception — a constraint violation, an unexpected data type, a model validation error — the entire block is rolled back and no partial data is written. * * * ## How It Works Plugin code runs inside a database context that sets the PostgreSQL `search_path` to the plugin's namespace. `transaction.atomic()` operates on this same connection automatically — no `using=` parameter is needed. Under the hood, `atomic()` issues a `SAVEPOINT` (for nested usage) or manages the transaction directly. When the block exits cleanly, the transaction is committed. When an exception propagates out, it is rolled back. * * * ## Nesting `atomic()` blocks can be nested. Inner blocks use PostgreSQL savepoints, so a failure in an inner block rolls back only that block's changes (not the entire outer transaction), provided you catch the exception: ```python from django.db.transaction import atomic with atomic(): Specialty.objects.create(name="Cardiology") try: with atomic(): Specialty.objects.create(name="Neurology") raise ValueError("something went wrong") except ValueError: pass # Only the "Neurology" insert is rolled back # "Cardiology" is still pending and will be committed ``` If the exception is **not** caught, it propagates to the outer block and rolls back everything. * * * ## See Also - [CustomModels](/sdk/custom-data-custom-models/) \- Defining structured models, relationships, and queries - [AttributeHubs](/sdk/custom-data-attribute-hubs/) \- Standalone key-value storage - [Design Considerations](/sdk/custom-data-design-considerations/) \- Choosing the right technique and avoiding anti-patterns - [Testing Custom Data](/sdk/custom-data-testing/) \- Testing utilities and examples --- # Custom Data Source: https://docs.canvasmedical.com/sdk/custom-data/ ## Overview The Canvas SDK provides two techniques for storing custom data in your plugins, allowing you to define fully structured data models with relationships among entities, or create flexible key-value stores: 1. **[CustomModels](/sdk/custom-data-custom-models/)** \- Build your own data model or expand the Canvas data model by adding fully structured tables with typed fields, relationships, and indexes 2. **[AttributeHubs](/sdk/custom-data-attribute-hubs/)** \- Store arbitrary key-value pairs and JSON data independently of the Canvas data model Each technique serves different use cases and provides different levels of structure and type safety. Both techniques may be used together. ## When to Use Each Technique ### CustomModels Use this when you need structured, typed data with relationships and normalized data. CustomModels can also extend existing SDK models (like Patient or Staff) with custom fields via `OneToOneField`, `ForeignKey`, and `ManyToManyField`. **Best for:** - Structured data with a stable, known schema - Custom fields on existing SDK models (e.g., provider preferences, patient flags) - Relationships between entities (foreign keys, join tables) - Data requiring compound filtering, sorting, or aggregation - Data consumed by reports or analytics **Example use cases:** - Provider specialties and certifications - Adding practice-specific fields to patients or staff - Linking `Staff` to `Note` creating a `supervising_provider` association - Custom workflows and forms - Integration-specific data structures - Practice-specific business operation concepts and logic [Learn more about CustomModels →](/sdk/custom-data-custom-models/) ### AttributeHubs AttributeHubs provide a key/value and document store free from the burden of defining any schema or linking to Canvas models. They are for storing irregular or unstructured information that doesn't have a natural home. Whereas CustomModels build upon the Canvas data model, AttributeHubs allow easy, standalone persistence of information. Use this when you need to store data that doesn't naturally belong to any existing or imagined model. **Best for:** - Cross-cutting state that spans multiple models (sync cursors, external IDs) - One-off or small-collection configuration and state - Data with no natural schema (varying fields per record) - External system state tracking **Example use cases:** - API synchronization state - External system identifiers - Plugin configuration and feature flags [Learn more about AttributeHubs →](/sdk/custom-data-attribute-hubs/) For help choosing between these techniques, see [Design Considerations](/sdk/custom-data-design-considerations/). For details on how multiple plugins can share a namespace using these keys, see the [Sharing Data](/sdk/custom-data-sharing-data/) guide. For managing API tokens and other sensitive configuration, see [Managing Secrets](/sdk/secrets/). ## Caching If your use case represents transient data that should expire via TTL, use the [Caching API](/sdk/caching) instead of the Custom Data features. ## Data Privacy and Plugin Isolation All custom data created by a plugin — whether using CustomModels or AttributeHubs — is scoped to a namespace. This isolation ensures that plugins cannot directly access or modify another plugin's data, maintaining security and data integrity across the system. Plugins may share data in two ways: - By explicit co-location within a namespace, allowing direct database access - By publishing [Simple API](/sdk/handlers-simple-api-http) endpoints [Learn more about data sharing](/sdk/custom-data-sharing-data) ### Data Isolation **CustomModels** created by a plugin exist within namespaces. Tables and data are completely isolated from other namespaces. ```python # In a plugin named "my_plugin": Creates a table "specialty" in the "my_plugin" namespace from canvas_sdk.v1.data.base import CustomModel from django.db.models import TextField class Specialty(CustomModel): name = TextField() ``` ```python # In a plugin named "your_plugin": Creates a table "specialty" in the "your_plugin" namespace from canvas_sdk.v1.data.base import CustomModel from django.db.models import TextField class Specialty(CustomModel): name = TextField() # In "your_plugin": Cannot access the "my_plugin" Specialty model or data ``` **AttributeHubs** similarly store data within the plugin's namespace and are not accessible to plugins in other namespaces. ## Testing Custom Data The Canvas SDK provides comprehensive testing utilities for all custom data approaches. See the [Testing Custom Data](/sdk/custom-data-testing/) guide for detailed examples and best practices. ## Sharing Data Use APIs to make data available and accessible to and from other plugins and external services. See the [Sharing Data](/sdk/custom-data-sharing-data/) guide for detailed examples and best practices. ## Read Replica Databases All the data managed by plugin is available via the database read replica. To access it, alter the PostgreSQL [search_path](https://www.postgresql.org/docs/18/ddl-schemas.html#DDL-SCHEMAS-PATH) to include the namespaces that you intend to query. ## Limitations (for Safety) - Values stored in `text` and `json` fields may not exceed 1mb as measured by character count. Going over raises `FieldValueTooLarge`. - Bulk operations (e.g., `bulk_create`) are limited to 10,000 records at a time. Going over raises `BulkOperationTooLarge`. Both are subclasses of `ValueError`, so a single `except ValueError:` catches either. ## See Also - [CustomModels](/sdk/custom-data-custom-models/) \- Structured models with relationships among entities - [Extending SDK Models](/sdk/custom-data-extending-sdk-models/) \- Proxy models, `related_name` namespacing, and referencing SDK models - [AttributeHubs](/sdk/custom-data-attribute-hubs/) \- Standalone key-value storage - [Design Considerations](/sdk/custom-data-design-considerations/) \- Choosing the right technique and avoiding anti-patterns - [Transactions](/sdk/custom-data-transactions/) \- All-or-nothing writes with `transaction.atomic()` - [Testing Custom Data](/sdk/custom-data-testing/) \- Testing utilities and examples - [Sharing Data](/sdk/custom-data-sharing-data/) \- Sharing data with other plugins and external services - [Data Models](/sdk/data/) \- Core SDK data models - [Caching API](/sdk/caching) \- Auto-expiring transient data - [Simple API](/sdk/handlers-simple-api/) \- Simple API for sharing data between plugins - [Secrets](/sdk/secrets/) \- Managing API keys and sensitive configuration --- # AllergyIntolerance Source: https://docs.canvasmedical.com/sdk/data-allergy-intolerance/ ## Introduction The `AllergyIntolerance` model represents a known risk, specific to a patient, of a harmful or undesirable physiological response associated with exposure to a substance. ## Basic usage To get an allergy intolerance by identifier, use the `get` method on the `AllergyIntolerance` model manager: ```python from canvas_sdk.v1.data.allergy_intolerance import AllergyIntolerance allergy = AllergyIntolerance.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the allergy intolerances for a patient can be accessed with the `allergy_intolerances` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") allergies = patient.allergy_intolerances.all() ``` If you have a patient ID, you can get the allergies for the patient with the `for_patient` method on the `AllergyIntolerance` model manager: ```python from canvas_sdk.v1.data.allergy_intolerance import AllergyIntolerance patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" allergies = AllergyIntolerance.objects.for_patient(patient_id) ``` ## Codings The codings for an allergy intolerance can be accessed with the `codings` attribute on an `AllergyIntolerance` object: ```python from canvas_sdk.v1.data.allergy_intolerance import AllergyIntolerance from logger import log allergy = AllergyIntolerance.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") for coding in allergy.codings.all(): log.info(f"system: {coding.system}") log.info(f"code: {coding.code}") log.info(f"display: {coding.display}") ``` ## Filtering Allergy intolerances can be filtered by any attribute that exists on the model. Filtering for allergy intolerances is done with the `filter` method on the `AllergyIntolerance` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.allergy_intolerance import AllergyIntolerance allergies = AllergyIntolerance.objects.filter(status="active") ``` ### By ValueSet See [Value Sets](/sdk/data-value-sets/) for the library of built-in value sets and how to create your own. Filtering by ValueSet works a little differently. The `find` method on the model manager is used to perform `ValueSet` filtering: ```python from canvas_sdk.v1.data.allergy_intolerance import AllergyIntolerance from canvas_sdk.value_set.v2022.allergy import EggSubstance allergies = AllergyIntolerance.objects.find(EggSubstance) ``` ## Attributes ### AllergyIntolerance Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note_id| Integer allergy_intolerance_type| String category| Integer status| String severity| String onset_date| Date onset_date_original_input| String last_occurrence| Date last_occurrence_original_input| String recorded_date| DateTime narrative| String codings| AllergyIntoleranceCoding[] remove_allergy_events| [RemoveAllergyEvent](/sdk/data-remove-allergy-event/#removeallergyevent)[] ### AllergyIntoleranceCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean allergy_intolerance| AllergyIntolerance --- # Application Source: https://docs.canvasmedical.com/sdk/data-application/ ## Introduction The `Application` model represents a plugin Application in Canvas. Applications are used to integrate third-party tools and services into the Canvas platform, allowing users to access external resources and functionalities directly from within Canvas. Each application has a unique identifier, name and description. ## Basic usage To get an application by identifier, use the `get` method on the `Application` model manager: ```python from canvas_sdk.v1.data import Application application = Application.objects.get(identifier="123") ``` ## Filtering Applications can be filtered by any attribute that exists on the model. Filtering for applications is done with the `filter` method on the `Application` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data import Application applications = Application.objects.filter(name="application name") ``` ## Attributes ### Application Field Name| Type ---|--- identifier| str name| str description| str created| datetime modified| datetime --- # Appointment Source: https://docs.canvasmedical.com/sdk/data-appointment/ ## Introduction The `Appointment` model represents a single scheduled meeting from a patient, that may be in the future or past. ## Basic usage To get an appointment by identifier, use the `get` method on the `Appointment` model manager: ```python from canvas_sdk.v1.data.appointment import Appointment appointment = Appointment.objects.get(id="f53626e4-0683-43ac-a1b7-c52815639ce2") ``` If you have a patient object, the appointments for a patient can be accessed with the `appointments` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") appointments = patient.appointments.all() ``` To get appointments part of a recurrence. ```python from canvas_sdk.v1.data.appointment import Appointment appointment = Appointment.objects.get(id="f53626e4-0683-43ac-a1b7-c52815639ce2") # parent appointment parent_appointment = appointment.parent_appointment # children appointments children = parent_appointment.children.all() ``` ## Getting the reason for visit The reason for visit is recorded on the appointment's note, not on the `Appointment` itself. Read the note's [`ReasonForVisit`](/sdk/data-reason-for-visit/) records through its `reasons_for_visit` attribute. The Reason for Visit command has no commit step, so read the records directly rather than filtering for committed ones: ```python from canvas_sdk.v1.data.appointment import Appointment appointment = Appointment.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") if appointment.note: reasons = appointment.note.reasons_for_visit.all() ``` ## Detecting rescheduled appointments What a reschedule looks like to a plugin depends on the operation used to move the appointment rather than on the surface the request came from. There are two shapes, and a plugin that needs to catch every reschedule has to handle both. **Cancel and create** fires both [`APPOINTMENT_RESCHEDULED`](/sdk/events/#appointments) and `APPOINTMENT_CREATED`, each targeting the newly created appointment. The original appointment's status is set to cancelled, and the new appointment's `appointment_rescheduled_from` points back at it. This is what happens with: - The reschedule flow in the provider UI. - The reschedule flow in the patient portal. - The [Appointment effect](/sdk/effect-notes/#reschedule-appointment)'s `reschedule()`. Listen for `APPOINTMENT_RESCHEDULED` rather than handling `APPOINTMENT_CREATED` and filtering on `appointment_rescheduled_from`. The purpose-built event already draws that distinction. **Update in place** fires only [`APPOINTMENT_UPDATED`](/sdk/events/#appointments). A single appointment record is modified, nothing is cancelled, no new appointment appears, and `APPOINTMENT_RESCHEDULED` does not fire even though the appointment moved to a different time. This is what happens with: - The [Appointment effect](/sdk/effect-notes/#update-appointment)'s `update()` carrying a new `start_time`. - A FHIR API appointment update, which changes the appointment's attributes to comply with FHIR. The SDK can produce either shape, so which one your plugin sees depends on what the calling code chose. A plugin that has to catch every move of an appointment needs to handle both `APPOINTMENT_RESCHEDULED` and `APPOINTMENT_UPDATED`. ## Filtering Appointments can be filtered by any attribute that exists on the model. Filtering for appointments is done with the `filter` method on the `Appointment` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.appointment import Appointment, AppointmentProgressStatus appointments = Appointment.objects.filter(status=AppointmentProgressStatus.CONFIRMED) ``` ### Filtering by External Identifiers To query Appointments by external identifiers, the `external_identifiers` relation can be used with double-underscores to identify values stored on the AppointmentExternalIdentifier model. For example: ```python from canvas_sdk.v1.data.appointment import Appointment appointment = Appointment.objects.filter( external_identifiers__system="COMPANY_IDENTIFIER", external_identifiers__value="ejNoTa5vKzoT9oSjg87MVB").first() ``` ## Attributes ### Appointment Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) appointment_rescheduled_from| Appointment parent_appointment| Appointment provider| Staff start_time| DateTime duration_minutes| Integer comment| String note_id| Integer note_type_id| Integer status| String meeting_link| URL telehealth_instructions_sent| Boolean location| PracticeLocation description| String external_identifiers| AppointmentExternalIdentifier[] metadata| AppointmentMetadata[] children| Appointment[] appointment_rescheduled_to| Appointment[] labels| [TaskLabel](/sdk/data-task/#tasklabel)[] ### AppointmentExternalIdentifier Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime use| String identifier_type| String system| String value| String issued_date| Date expiration_date| Date appointment| Appointment ### AppointmentLabel The join between an appointment and a [TaskLabel](/sdk/data-task/#tasklabel). Reach the labels on an appointment through its `labels` attribute rather than querying this model. Field Name| Type ---|--- dbid| Integer appointment| Appointment task_label| [TaskLabel](/sdk/data-task/#tasklabel) ### AppointmentMetadata Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime appointment| Appointment key| String value| String ```python from canvas_sdk.v1.data.appointment import Appointment from logger import log appointment_id = "f53626e4-0683-43ac-a1b7-c52815639ce2" appointment = Appointment.objects.get(id=appointment_id) appointment_metadata = appointment.metadata.all() for metadata in appointment_metadata: log.info(f"Appointment metadata: {metadata.key}, {metadata.value}") ``` --- # Assessment Source: https://docs.canvasmedical.com/sdk/data-assessment/ ## Introduction The `Assessment` model represents a clinical assessment or evaluation of a patient's medical `Condition`. ## Basic usage To get an assessment by identifier, use the `get` method on the `Assessment` model manager: ```python from canvas_sdk.v1.data.assessment import Assessment assessment = Assessment.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the assessments for a patient can be accessed with the `assessments` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") assessments = patient.assessments.all() ``` ## Filtering Assessments can be filtered by any attribute that exists on the model. Filtering for assessments is done with the `filter` method on the `Assessment` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.assessment import Assessment, AssessmentStatus assessments = Assessment.objects.filter(patient__id="1eed3ea2a8d546a1b681a2a45de1d790", status=AssessmentStatus.STATUS_IMPROVING) ``` ### Committed assessments The `committed` method returns assessments that have been committed and not entered in error: ```python from canvas_sdk.v1.data.assessment import Assessment committed_assessments = Assessment.objects.committed() ``` ## Attributes ### Assessment Field Name| Type| ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| originator| [CanvasUser](/sdk/data-canvasuser)| entered_in_error| [CanvasUser](/sdk/data-canvasuser)| committer| [CanvasUser](/sdk/data-canvasuser)| patient| [Patient](/sdk/data-patient/#patient)| note| [Note](/sdk/data-note/#note)| condition| [Condition](/sdk/data-condition/#condition)| interview| [Interview](/sdk/data-questionnaire/#interview)| status| AssessmentStatus| narrative| String| background| String| care_team| String| treatments_stated| [MedicationStatement](/sdk/data-medication-statement)[]| billinglineitem_set| [BillingLineItem](/sdk/data-billing-line-item)[]| referrals| [Referral](/sdk/data-referral)[]| ### Commands linked to an assessment Plan- and Procedures-section commands, and the order, medication, and educational material commands, can be linked to an assessment made in the same note. The command holds the link on its own `assessment` field, and each accessor below reads the link back from the assessment's side. A link only joins a command and an assessment in the same note. When a command moves to another note without its assessment, or an assessment moves without the commands linked to it, Canvas clears those links. A command and its assessment that move together stay linked. [Carrying a command forward](/sdk/commands/#command-actions) into a new note copies its data but not its link, so the new command starts with no `assessment`. Linking a prescription or an order also adds the assessed condition to the command's diagnosis codes. Each command's `assessment_id` parameter in the [commands module](/sdk/commands/) gives the details. Clearing the link leaves those codes in place. On an instance in the [revamped note beta program](/product-updates/note-revamp/), a [printed note](/sdk/commands/#command-actions) in the revamped format lists each linked command indented under its assessment. Field Name| Type ---|--- follow_ups| [FollowUp](/sdk/data-follow-up/#followup)[] goals| [Goal](/sdk/data-goal/#goal)[] immunizations| [Immunization](/sdk/data-immunization/#immunization)[] instructions| [Instruction](/sdk/data-instruction/#instruction)[] linked_change_medications| [ChangeMedication](/sdk/data-change-medication/#changemedication)[] linked_dme_orders| [DmeOrder](/sdk/data-dme/#dmeorder)[] linked_educational_materials| [EducationalMaterial](/sdk/data-educational-material/#educationalmaterial)[] linked_imaging_orders| [ImagingOrder](/sdk/data-imaging/#imagingorder)[] linked_lab_orders| [LabOrder](/sdk/data-labs/#laborder)[] linked_prescriptions| [Prescription](/sdk/data-prescription/#prescription)[] linked_referrals| [Referral](/sdk/data-referral/#referral)[] note_tasks| [NoteTask](/sdk/data-task/#notetask)[] plans| [Plan](/sdk/data-plan/#plan)[] procedures| [Procedure](/sdk/data-procedure/#procedure)[] stopped_medications| [StopMedicationEvent](/sdk/data-stop-medication-event/#stopmedicationevent)[] updategoals| [UpdateGoal](/sdk/data-goal/#updategoal)[] Use `linked_referrals` for referrals linked this way. The `referrals` accessor above reads a referral's separate `assessments` list instead. A linked [Close Goal](/sdk/commands/#closegoal) command arrives on `updategoals` rather than through an accessor of its own, because [both the Update Goal and Close Goal commands record an `UpdateGoal`](/sdk/data-goal/#goal-updates-and-closures). ## Enumeration types ### Assessment Status Value| Label ---|--- STATUS_IMPROVING| Improved STATUS_STABLE| Unchanged STATUS_DETERIORATING| Deteriorated --- # BannerAlert Source: https://docs.canvasmedical.com/sdk/data-banner-alert/ ## Introduction The `BannerAlert` model represents alerts associated with [Patient](/sdk/data-patient/#patient) records. This page deals with data retrieval. To create or remove `BannerAlert` records, see [Banner Alert Effects](/sdk/effect-banner-alerts/). ## Usage The `BannerAlert` model can be used to find all of the banner alert records linked to a patient. For example, to find all of the banner alerts for a patient, the `Patient.banner_alerts` method can be used: ```python >>> from canvas_sdk.v1.data.patient import Patient >>> patient_1 = Patient.objects.get(id="aebe4d3f5d18410388dc69c4b5169fc3") >>> patient_banner_alerts = patient_1.banner_alerts.all() >>> print([item.narrative for item in patient_banner_alerts]) ['Patient spits when angry', 'Confirm contact info'] ``` ## Filtering The `filter` method can be used to filter by desired attributes. The following examples show commonly used operations to filter banner alert data: **Show a Patient's active BannerAlert records from the 'foo' plugin in order of descending creation date** ```python >>> from canvas_sdk.v1.data.patient import Patient >>> patient_1 = Patient.objects.get(id="aebe4d3f5d18410388dc69c4b5169fc3") >>> banner_alerts = patient_1.banner_alerts.filter(status='active', plugin_name='foo').order_by("created") >>> print([item.narrative for item in banner_alerts]) ['foo', 'bar'] ``` ## Attributes ### BannerAlert Field Name| Type ---|--- dbid| Integer id| UUID created| DateTime modified| DateTime patient| [Patient](/sdk/data-patient/#patient) plugin_name| String key| String narrative| String placement| BannerAlertPlacement[] intent| BannerAlertIntent href| String status| BannerAlertStatus ## Enumeration types ### BannerAlertStatus Value| Label ---|--- active| Active inactive| Inactive ### BannerAlertIntent Value| Label ---|--- info| Info warning| Warning alert| Alert ### BannerAlertPlacement Value| Label ---|--- chart| Chart timeline| Timeline appointment_card| Appointment Card scheduling_card| Scheduling Card profile| Profile --- # BillingLineItem Source: https://docs.canvasmedical.com/sdk/data-billing-line-item/ ## Introduction The `BillingLineItem` model represents billing line items linked to [Notes](/sdk/data-note) that can be found in the note footer. BillingLineItems are also linked to [Patient](/sdk/data-patient/#patient) instances. ## Usage The `BillingLineItem` model can be used to find all of the billable codes linked to a patient note. For example, to find all of the current billing line items for a note, the `Note.billing_line_items` method can be used: ```python >>> from canvas_sdk.v1.data.note import Note >>> from canvas_sdk.v1.data.billing import BillingLineItemStatus >>> note_1 = Note.objects.get(id="a74592ae8a6c4d0ebe0799d3fb3713d1") >>> note_1_billing_line_items = note_1.billing_line_items.filter(status=BillingLineItemStatus.ACTIVE) >>> print([item.cpt for item in note_1_billing_line_items]) ['99213', '90703'] ``` Alternatively, you could find all the `BillingLineItem` instances for a single `Patient`: ```python >>> from canvas_sdk.v1.data.patient import Patient >>> patient_1 = Patient.objects.get(id="aebe4d3f5d18410388dc69c4b5169fc3") >>> patient_billing_line_items = patient_1.billing_line_items.all() >>> print([item.cpt for item in patient_billing_line_items]) ['99213', '90703', '76942', '67505'] ``` You can also access all `BillingLineItemModifier`s associated with a `BillingLineItem`: ```python >>> from canvas_sdk.v1.data.billing import BillingLineItem, BillingLineItemModifier >>> line_item = BillingLineItem.objects.get(id="b80b1cdc2e6a4aca90ccebc02e683f35") >>> line_item_modifiers = line_item.modifiers.all() >>> print([mod.code for mod in line_item_modifiers]) ['25', '59'] ``` ## Filtering The `filter` method can be used to filter by desired attributes. The following examples show commonly used operations to filter billing line item data: **Show a Patient's active BillingLineItems that start with '99-' in order of descending charge amount** ```python >>> from canvas_sdk.v1.data.patient import Patient >>> from canvas_sdk.v1.data.billing import BillingLineItemStatus >>> patient_1 = Patient.objects.get(id="aebe4d3f5d18410388dc69c4b5169fc3") >>> office_visit_charges = patient_1.billing_line_items.filter(status=BillingLineItemStatus.ACTIVE, cpt__startswith='99').order_by("charge") >>> print([(item.cpt, item.charge,) for item in office_visit_charges]) [('99215', 200.00), ('99215', 190.00), ('99214', 100.00), ('99213', 80.00)] ``` **Find All Removed BillingLineItems from a Note** ```python >>> import arrow >>> from canvas_sdk.v1.data.note import Note >>> from canvas_sdk.v1.data.billing import BillingLineItemStatus >>> note_1 = Note.objects.get(id="a74592ae8a6c4d0ebe0799d3fb3713d1") >>> note_1_removed_items = note_1.billing_line_items.filter(status=BillingLineItemStatus.REMOVED) >>> print([item.cpt for item in note_1_removed_items]) ['11901', '00950'] ``` For examples of how to use the BillingLineItem data class with the BillingLineItem effects, check out [this page](/sdk/effect-billing-line-items) ## Attributes ### BillingLineItem Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime note| [Note](/sdk/data-note) patient| [Patient](/sdk/data-patient/#patient) cpt| String charge| Decimal description| String units| Integer command_type| String command_id| Integer status| BillingLineItemStatus assessments| [Assessment](/sdk/data-assessment)[] modifiers| BillingLineItemModifier[] claimlineitem_set| [ClaimLineItem](/sdk/data-claim/#claimlineitem)[] ### BillingLineItemModifier Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime line_item| BillingLineItem system| String version| String code| String display| String user_selected| Boolean ## Enumeration types ### BillingLineItemStatus Value| Label ---|--- ACTIVE| Active REMOVED| Removed --- # BusinessLine Source: https://docs.canvasmedical.com/sdk/data-business-line/ ## Introduction The `BusinessLine` model represents a group of [Patients](/sdk/data-patient/#patient) that share a common brand under an [Organization](/sdk/data-organization). ## Usage The `BusinessLine` model can be used to find all of the patients for a given Business Line: ```python >>> from canvas_sdk.v1.data import BusinessLine >>> business_line = BusinessLine.objects.get(id="ff844d60Od18466698dc645PtYZ3019Tt") >>> business_line_patients = business_line.patients.all() >>> print([patient.first_name for patient in business_line_patients]) ['George', 'Louise', 'Julia'] ``` You can also access a patient's Business Line from the `Patient` model: ```python >>> from canvas_sdk.v1.data import Patient >>> patient_1 = Patient.objects.get(id="aebe4d3f5d18410388dc69c4b5169fc3") >>> patient_business_line = patient_1.business_line >>> print(patient_business_line.name) 'New Patients that love cheese' ``` And you can also access all of the Business Lines under a given Organization: ```python >>> from canvas_sdk.v1.data import Organization >>> organization = Organization.objects.first() >>> organization_business_lines = organization.business_lines.all() >>> print([business_line.name for business_line in organization_business_lines]) ['New Patients that love cheese', 'Spanish Speaking Patients', 'One Medical'] ``` ## Accessing the logo The `logo_url` property returns a presigned S3 URL for the business line's logo image, or `None` when no logo is set. Use it to brand plugin output, such as a patient portal page or a generated document, with the logo configured in Canvas. The URL is valid for one hour, so generate it when you need it rather than storing it. ```python from canvas_sdk.v1.data import Patient patient = Patient.objects.get(id="aebe4d3f5d18410388dc69c4b5169fc3") # Presigned S3 URL to the logo image, or None if no logo is set logo_url = patient.business_line.logo_url if patient.business_line else None ``` ## Filtering The `filter` method can be used to filter by desired attributes. The following examples show commonly used operations to filter Business Line data: **Show an Organization's Business Lines that are active and in the 732 area code** ```python >>> from canvas_sdk.v1.data import BusinessLine, Organization >>> org = Organization.objects.first() >>> active_732_business_lines = BusinessLine.objects.filter(organization=org, active=True, area_code="732") >>> print([business_line.name for business_line in active_732_business_lines]) ['Foo', 'Bar'] ``` ## Attributes ### BusinessLine Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime name| String description| String area_code| String subdomain| String active| Boolean state| BusinessLineState organization| [Organization](/sdk/data-organization) logo| File (S3 key) logo_url| String (computed) patients| QuerySet[[Patient](/sdk/data-patient/#patient)] ## Enumeration types ### BusinessLineState Value| Label ---|--- success| Success pending| Pending error| Deleted --- # Calendar Source: https://docs.canvasmedical.com/sdk/data-calendar/ ## Introduction The `Calendar` model represents a Calendar in Canvas. Calendars are used to organize events for providers and can be either Clinic or Administrative type. Busy time that native Google Calendar sync imports for a provider isn't stored as calendar events. To read it, use the [Google Calendar](/sdk/data-google-calendar/) data models. ## Basic usage To get a calendar by identifier, use the `get` method on the `Calendar` model manager: ```python from canvas_sdk.v1.data.calendar import Calendar calendar = Calendar.objects.get(id="f53626e4-0683-43ac-a1b7-c52815639ce2") ``` ## Events The events associated with a calendar can be accessed with the `events` attribute on a `Calendar` object: ```python from canvas_sdk.v1.data.calendar import Calendar calendar = Calendar.objects.get(id="f53626e4-0683-43ac-a1b7-c52815639ce2") events = calendar.events.all() ``` To get a specific event by identifier: ```python from canvas_sdk.v1.data.calendar import Event event = Event.objects.get(id="a1b2c3d4-5678-90ab-cdef-1234567890ab") ``` ## Filtering Calendars and events can be filtered by any attribute that exists on the model. Filtering is done with the `filter` method on the model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.calendar import Calendar # Filter calendars by title calendars = Calendar.objects.filter(title__icontains="Clinic") ``` ### By calendar name To filter calendars by a specific provider name, calendar type, and location, use the `for_calendar_name` method: ```python from canvas_sdk.v1.data.calendar import Calendar calendars = Calendar.objects.for_calendar_name( provider_name="Dr. Smith", calendar_type="Clinic", location="Main Office" ) ``` ## Attributes ### Calendar Field Name| Type| Description ---|---|--- id| UUID| Unique identifier for the calendar dbid| Integer| Database identifier created| DateTime| When the record was created. modified| DateTime| When the record was last modified. title| String| The title of the calendar timezone| TimeZone| The timezone for the calendar (default: UTC) description| String| Optional description of the calendar's purpose events| Event[]| Events associated with this calendar ### Event Field Name| Type| Description ---|---|--- id| UUID| Unique identifier for the event dbid| Integer| Database identifier created| DateTime| When the record was created. modified| DateTime| When the record was last modified. title| String| The title of the event description| String| Description of the event calendar| Calendar| The calendar this event belongs to starts_at| DateTime| The start date and time of the event ends_at| DateTime| The end date and time of the event recurrence| String| Recurrence rule for recurring events recurrence_ends_at| DateTime| The date and time when the recurrence pattern ends recurring_parent_event| Event| The parent event exceptions| Event[]| Exception (override) events for this recurring parent event original_starts_at| DateTime| The original start time for recurring event exceptions is_all_day| Boolean| Whether this is an all-day event (default: false) is_cancelled| Boolean| Whether this event has been cancelled (default: false) allowed_note_types| NoteType[]| Note types that are allowed for this event ## Examples ### Working with calendar events ```python from canvas_sdk.v1.data.calendar import Calendar from datetime import datetime from logger import log calendar = Calendar.objects.get(id="f53626e4-0683-43ac-a1b7-c52815639ce2") # Get all upcoming events upcoming_events = calendar.events.filter( starts_at__gte=datetime.now(), is_cancelled=False ).order_by('starts_at') for event in upcoming_events: log.info(f"Event: {event.title}") log.info(f"Starts at: {event.starts_at}") log.info(f"Ends at: {event.ends_at}") if event.recurrence: log.info(f"Recurrence: {event.recurrence}") ``` --- # CancelPrescriptionResponse Source: https://docs.canvasmedical.com/sdk/data-cancel-prescription-response/ ## Introduction The `CancelPrescriptionResponse` model captures the response to a [CancelPrescription](/sdk/data-cancel-prescription) request. Each response is linked one-to-one to the request that produced it. ## Basic usage To get a cancel prescription response by identifier, use the `get` method on the `CancelPrescriptionResponse` model manager: ```python from canvas_sdk.v1.data import CancelPrescriptionResponse response = CancelPrescriptionResponse.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the cancel prescription responses for a patient can be accessed with the `cancel_prescription_responses` attribute: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") responses = patient.cancel_prescription_responses.all() ``` Or, from a cancel prescription, reach its response with the `response` attribute: ```python from canvas_sdk.v1.data import CancelPrescription cancel = CancelPrescription.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") response = cancel.response ``` ## Attributes ### CancelPrescriptionResponse Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime patient| [Patient](/sdk/data-patient/#patient) request| [CancelPrescription](/sdk/data-cancel-prescription) message_id| String note| String reason_code| String response| String --- # CancelPrescription Source: https://docs.canvasmedical.com/sdk/data-cancel-prescription/ ## Introduction The `CancelPrescription` model is the anchor for the CancelPrescription command — a request to cancel a patient's prescription, recorded on a Note. ## Basic usage To get a cancel prescription by identifier, use the `get` method on the `CancelPrescription` model manager: ```python from canvas_sdk.v1.data import CancelPrescription cancel = CancelPrescription.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the cancel prescriptions for a patient can be accessed with the `cancel_prescriptions` attribute: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") cancels = patient.cancel_prescriptions.all() ``` Or, from a prescription, reach its cancellations with the same `cancel_prescriptions` attribute: ```python from canvas_sdk.v1.data import Prescription prescription = Prescription.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") cancels = prescription.cancel_prescriptions.all() ``` ## Committed records The `committed` method returns cancel prescriptions that have been committed and not entered in error: ```python from canvas_sdk.v1.data import CancelPrescription committed_cancels = CancelPrescription.objects.committed() ``` ## Attributes ### CancelPrescription Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) prescription| [Prescription](/sdk/data-prescription) message_id| String status| CancelPrescriptionStatus response| [CancelPrescriptionResponse](/sdk/data-cancel-prescription-response/#cancelprescriptionresponse) ## Enumeration types ### CancelPrescriptionStatus Name| Value ---|--- OPEN| open PENDING| pending ULTIMATELY_ACCEPTED| ultimately-accepted --- # CanvasUser Source: https://docs.canvasmedical.com/sdk/data-canvasuser/ ## Introduction The `CanvasUser` model represents a Canvas User. This could be linked to a staff member or a patient. This model isn't meant to be referenced directly, but is sometimes used to attribute a record to user. ## Basic usage To get a user by identifier, use the `get` method on the `CanvasUser` model manager: ```python from canvas_sdk.v1.data import CanvasUser user = CanvasUser.objects.get(dbid=123) ``` ## Filtering Users can be filtered by any attribute that exists on the model. Filtering for users is done with the `filter` method on the `CanvasUser` model manager. ### By attribute Specify attributes with `filter` to filter by those attributes: ```python from canvas_sdk.v1.data import CanvasUser users = CanvasUser.objects.filter(phone_number="1111111111", email="test@canvasmedical.com") ``` ## Attributes ### User Field Name| Type ---|--- dbid| Integer email| String phone_number| String is_staff| Boolean is_portal_registered| Boolean last_invite_date_time| DateTime person_subclass| [Staff](/sdk/data-staff/#staff) | [Patient](/sdk/data-patient/#patient) staff| [Staff](/sdk/data-staff/#staff) patient| [Patient](/sdk/data-patient/#patient) sent_messages| [Message](/sdk/data-message/#message)[] received_messages| [Message](/sdk/data-message/#message)[] commands_originated| [Command](/sdk/data-command/#command)[] commands_committed| [Command](/sdk/data-command/#command)[] commands_entered_in_error| [Command](/sdk/data-command/#command)[] --- # CareTeam Source: https://docs.canvasmedical.com/sdk/data-care-team/ ## Introduction The `CareTeam` model represents a collection of [Staff](/sdk/data-staff/#staff) that are responsible for the care of a [Patient](/sdk/data-patient/#patient). ## Usage There are 2 data models associated with Care Teams - `CareTeamMembership` and `CareTeamRole`. The `CareTeamRole` model stores all of the available roles that are available to be filled by staff members (i.e. _Physician_ , _Nurse Practitioner_ , etc.). For example, the following code will show the names of active roles that are available in a Canvas instance: ```python >>> from canvas_sdk.v1.data.care_team import CareTeamRole >>> active_care_team_roles = CareTeamRole.objects.filter(active=True) >>> role_names = [role.display for role in active_care_team_roles] >>> print(role_names) ['Primary care physician', 'Physician', 'Physician assistant', 'Nurse practitioner', 'Health coach', 'Care coordinator'] ``` The `CareTeamMembership` model connects patients, staff members and their associated roles to make up the assembly of a patient's Care Team. To retrieve staff members and their respective roles on a patient's care team, the `care_team_memberships` attribute available on a [Patient](/sdk/data-patient/#patient) instance can be used: ```python >>> from canvas_sdk.v1.data.patient import Patient >>> patient_1 = Patient.objects.get(id="a74592ae8a6c4d0ebe0799d3fb3713d1") >>> patient_1_care_team = patient_1.care_team_memberships.all() >>> print([(ctm.role.display, ctm.staff,) for ctm in patient_1_care_team]) [('Primary care physician', ), ('Nurse practitioner', ), ('Physician assistant', )] ``` ### External care team members A care team can also include external members — providers who are not [Staff](/sdk/data-staff#staff) on the Canvas instance. An external membership has no `staff`. Instead, its `organizational_entity` links to an [OrganizationalEntity](/sdk/data-organizational-entity/#organizationalentity) that describes the external provider. When that entity is a [ServiceProvider](/sdk/data-serviceprovider/#service-provider), the membership's `service_provider` property resolves directly to it, so you can read the provider's contact details — such as `business_fax` — without leaving the plugin. ```python >>> from canvas_sdk.v1.data.patient import Patient >>> patient_1 = Patient.objects.get(id="a74592ae8a6c4d0ebe0799d3fb3713d1") >>> external_member = patient_1.care_team_memberships.filter(staff__isnull=True).first() >>> external_member.service_provider.business_fax '18005551234' ``` The `service_provider` property returns `None` for internal (staff-backed) memberships, and for external members whose organizational entity is not a `Service Provider`. ## Filtering The `filter` method can be used to filter by desired attributes. The following examples show commonly used operations to filter care team data: **Find a patient's care team lead** ```python >>> from canvas_sdk.v1.data.patient import Patient >>> from canvas_sdk.v1.data.care_team import CareTeamMembershipStatus >>> patient_1 = Patient.objects.get(id="a74592ae8a6c4d0ebe0799d3fb3713d1") >>> patient_1_care_team_lead = patient_1.care_team_memberships.filter(lead=True, status=CareTeamMembershipStatus.ACTIVE).first() >>> assert patient_1_care_team_lead is not None >>> print((patient_1_care_team_lead.staff, patient_1_care_team_lead.role,)) (, ) ``` **Find all Patients that have a Certain Staff Member on their Care Team** ```python >>> from canvas_sdk.v1.data.staff import Staff >>> from canvas_sdk.v1.data.care_team import CareTeamMembershipStatus >>> staff_member = Staff.objects.get(id="3640cd20de8a470aa570a852859ac87e") >>> staff_care_teams = staff_member.care_team_memberships.filter(status=CareTeamMembershipStatus.ACTIVE) >>> print([(ctm.patient, ctm.lead,) for ctm in staff_care_teams]) [(, True), (, False)] ``` ## Attributes ### CareTeamRole Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| BooleanField active| Boolean care_teams| [CareTeamMembership](/sdk/data-care-team/#careteammembership)[] ### CareTeamMembership Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| patient| [Patient](/sdk/data-patient/#patient)| staff| [Staff](/sdk/data-staff#staff)| role| CareTeamRole| organizational_entity| [OrganizationalEntity](/sdk/data-organizational-entity/#organizationalentity)| status| CareTeamMembershipStatus| lead| Boolean| role_code| String| role_system| String| role_display| String| service_provider| [ServiceProvider](/sdk/data-serviceprovider/#service-provider) | `None` (computed)| The external provider for this membership, resolved through its `organizational_entity`; `None` for internal members. For external (non-staff) members, `staff` is empty and `organizational_entity` links to the external provider. ## Enumeration types ### CareTeamMembershipStatus Value| Label ---|--- proposed| Proposed active| Active suspended| Suspended inactive| Inactive entered-in-error| Entered in Error --- # Change Medication Source: https://docs.canvasmedical.com/sdk/data-change-medication/ ## Introduction The `ChangeMedication` model represents a record of a Change Medication command, used to update the directions (sig) of a medication already on a patient's medication list without issuing a new prescription. ## Basic usage To get a change medication by identifier, use the `get` method on the `ChangeMedication` model manager: ```python from canvas_sdk.v1.data import ChangeMedication change_medication = ChangeMedication.objects.get(id="61a1853f-168f-4ed3-80d2-44e5d144bcf3") ``` If you have a patient object, the change medications for a patient can be accessed with the `change_medications` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") change_medications = patient.change_medications.all() ``` You can also access the referenced medication with the `medication` attribute: ```python from canvas_sdk.v1.data import ChangeMedication change_medication = ChangeMedication.objects.get(id="61a1853f-168f-4ed3-80d2-44e5d144bcf3") medication = change_medication.medication ``` Or for a given medication, you can access all change medication records: ```python from canvas_sdk.v1.data import Medication medication = Medication.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") change_medications = medication.change_medications.all() ``` ## Latest sig A change medication's sig is one of the sources a medication's [`latest_sig`](/sdk/data-medication/#latest-sig) draws on, alongside its active prescriptions and medication statements. To get the most recent sig for the medication, read `latest_sig` from the referenced medication: ```python from canvas_sdk.v1.data import ChangeMedication change_medication = ChangeMedication.objects.get(id="61a1853f-168f-4ed3-80d2-44e5d144bcf3") sig = change_medication.medication.latest_sig ``` ## Committed records The `committed` method returns change medications that have been committed and not entered in error: ```python from canvas_sdk.v1.data import ChangeMedication committed_change_medications = ChangeMedication.objects.committed() ``` ## Attributes ### ChangeMedication Field Name| Type ---|--- id| UUID dbid| Integer patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) assessment| [Assessment](/sdk/data-assessment/#assessment) medication| [Medication](/sdk/data-medication) entered_in_error| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) originator| [CanvasUser](/sdk/data-canvasuser) created| DateTime modified| DateTime sig_original_input| String --- # ChargeDescriptionMaster Source: https://docs.canvasmedical.com/sdk/data-charge-description-master/ ## Introduction The `ChargeDescriptionMaster` model represents a billing charge in Canvas that can be added to the note footer. ## Usage The `ChargeDescriptionMaster` model can be filtered by any of its attributes, including `cpt_code`, `name`, and `short_name`: ```python >>> from canvas_sdk.v1.data import ChargeDescriptionMaster >>> office_visit_charges = ChargeDescriptionMaster.objects.filter(cpt_code__startswith="99") >>> print([charge.short_name for charge in office_visit_charges]) ["Office outpatient visit 40 minutes", "Office outpatient visit 25 minutes", "Office outpatient visit 10 minutes"] ``` You can also access `PayorSpecificCharge`s from the `ChargeDescriptionMaster` model: ```python >>> from canvas_sdk.v1.data import ChargeDescriptionMaster >>> office_visit_40min = ChargeDescriptionMaster.objects.filter(cpt_code="99215").first() >>> payor_specific_charges = office_visit_40min.transactor_charges.all() >>> print([charge.transactor.name for charge in payor_specific_charges]) ["Aetna", "Medicare", "Blue Shield of CA"] ``` ` ## Attributes ### ChargeDescriptionMaster Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime cpt_code| String name| String short_name| String charge_amount| Decimal effective_date| Date end_date| Date code_system| CDMCodeSystem ndc_code| String transactor_charges| QuerySet[[PayorSpecificCharge](/sdk/data-payor-specific-charge/#payorspecificcharge)] vaccines| QuerySet[[Vaccine](/sdk/data-vaccine/#vaccine)] ## Enumeration types ### CDMCodeSystem Value| Label ---|--- INTERNAL| Internal CPT| CPT --- # ChartSectionReview Source: https://docs.canvasmedical.com/sdk/data-chart-section-review/ ## Introduction The `ChartSectionReview` model represents a reviewed chart section captured on a note, with its pre-rendered content. When a provider reviews a chart section during a visit, Canvas stores a snapshot of that section's content at the time of review. ## Basic usage To get a chart section review by identifier, use the `get` method on the `ChartSectionReview` model manager: ```python from canvas_sdk.v1.data.chart_section_review import ChartSectionReview review = ChartSectionReview.objects.get(id="b5a0c1d2-e3f4-5678-9abc-def012345678") ``` If you have a patient object, the chart section reviews for a patient can be accessed with the `chart_section_reviews` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") reviews = patient.chart_section_reviews.all() ``` If you have a note object, the chart section reviews for that note can be accessed with the `chart_section_reviews` attribute on a `Note` object: ```python from canvas_sdk.v1.data.note import Note note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") reviews = note.chart_section_reviews.all() ``` ## Filtering Chart section reviews can be filtered by any attribute that exists on the model. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.chart_section_review import ( ChartSectionReview, ChartSectionReviewSection, ) # Get all reviews for the conditions section condition_reviews = ChartSectionReview.objects.filter( section=ChartSectionReviewSection.CONDITIONS ) ``` ### By patient and section ```python from canvas_sdk.v1.data.chart_section_review import ( ChartSectionReview, ChartSectionReviewSection, ) from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") medication_reviews = ChartSectionReview.objects.filter( patient=patient, section=ChartSectionReviewSection.MEDICATIONS ) ``` ### Committed reviews The `committed` method returns chart section reviews that have been committed and not entered in error: ```python from canvas_sdk.v1.data.chart_section_review import ChartSectionReview committed_reviews = ChartSectionReview.objects.committed() ``` ## Working with entries `entries` is a list of integer `dbid` values identifying the records that were reviewed in the section. Which model those `dbid`s belong to depends on the review's `section`: `section`| Model(s) referenced by `entries` ---|--- `conditions`| [Condition](/sdk/data-condition/) (non-surgical) `surgical_history`| [Condition](/sdk/data-condition/) (surgical) `medications`| [Medication](/sdk/data-medication/) `family_histories`| [FamilyHistory](/sdk/data-family-history/#familyhistory) `allergies`| [AllergyIntolerance](/sdk/data-allergy-intolerance/) `immunizations`| [Immunization](/sdk/data-immunization/) and [ImmunizationStatement](/sdk/data-immunization/) If you only need to **see what was reviewed** , prefer the `content` field: it holds the pre-rendered text of the reviewed records (one line per entry, captured at review time), so it needs no entry resolution and avoids the ambiguity described below. To work with the **record objects themselves** , filter the corresponding model by `dbid__in=review.entries`. Always scope the query to `review.patient` as well: a `dbid` is unique only within its own table, so scoping by patient avoids matching an unrelated record that happens to share the same integer (this is also why the `immunizations` section, which draws from two models, is resolved against both). ```python from canvas_sdk.v1.data.allergy_intolerance import AllergyIntolerance from canvas_sdk.v1.data.chart_section_review import ( ChartSectionReview, ChartSectionReviewSection, ) from canvas_sdk.v1.data.condition import Condition from canvas_sdk.v1.data.family_history import FamilyHistory from canvas_sdk.v1.data.immunization import Immunization, ImmunizationStatement from canvas_sdk.v1.data.medication import Medication review = ChartSectionReview.objects.get(id="b5a0c1d2-e3f4-5678-9abc-def012345678") if review.section == ChartSectionReviewSection.CONDITIONS: records = Condition.objects.filter( patient=review.patient, dbid__in=review.entries, surgical=False ) elif review.section == ChartSectionReviewSection.SURGICAL_HISTORY: records = Condition.objects.filter( patient=review.patient, dbid__in=review.entries, surgical=True ) elif review.section == ChartSectionReviewSection.MEDICATIONS: records = Medication.objects.filter(patient=review.patient, dbid__in=review.entries) elif review.section == ChartSectionReviewSection.FAMILY_HISTORIES: records = FamilyHistory.objects.filter(patient=review.patient, dbid__in=review.entries) elif review.section == ChartSectionReviewSection.ALLERGIES: records = AllergyIntolerance.objects.filter(patient=review.patient, dbid__in=review.entries) elif review.section == ChartSectionReviewSection.IMMUNIZATIONS: # entries may reference either model, so resolve against both. records = [ *Immunization.objects.filter(patient=review.patient, dbid__in=review.entries), *ImmunizationStatement.objects.filter(patient=review.patient, dbid__in=review.entries), ] ``` > **Note on the`immunizations` section:** `entries` stores bare `dbid` integers with no indication of which model each one came from. Because `Immunization` and `ImmunizationStatement` are separate tables with independent `dbid` sequences, the same integer can be a valid `dbid` in both. If a patient has an `Immunization` and an `ImmunizationStatement` that share a `dbid` and only one was reviewed, resolving against both models (as above) will return both records — there is no way to disambiguate them from `entries` alone. Treat the immunizations result as a best-effort superset, not an exact match. If you only need to know what was reviewed, use `content` instead: it captured the rendered text of the actual reviewed items at review time. ## Attributes ### ChartSectionReview Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note/#note) section| ChartSectionReviewSection entries| Integer[] (`dbid`s of the reviewed records — see Working with entries) content| String (newline-separated bullet items) originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) ## Enumeration types ### ChartSectionReviewSection Value| Label ---|--- conditions| Conditions surgical_history| Surgical History medications| Medications family_histories| Family Histories allergies| Allergies immunizations| Immunizations --- # Claim Source: https://docs.canvasmedical.com/sdk/data-claim/ ## Introduction This module defines the data models used to manage healthcare claim workflows. ## Basic usage To retrieve a claim by its identifier: ```python from canvas_sdk.v1.data.claim import Claim claim = Claim.objects.get(id="9d2e0f58-338b-11ec-8d3d-0242ac130003") ``` To access diagnosis codes for a claim: ```python from canvas_sdk.v1.data.claim import Claim claim = Claim.objects.get(id="9d2e0f58-338b-11ec-8d3d-0242ac130003") diagnosis_codes = claim.diagnosis_codes.all().order_by("rank") for diagnosis in diagnosis_codes: print(f"Rank {diagnosis.rank}: {diagnosis.code} - {diagnosis.display}") ``` To access banner alerts for a claim: ```python from canvas_sdk.v1.data.claim import Claim claim = Claim.objects.get(id="9d2e0f58-338b-11ec-8d3d-0242ac130003") active_alerts = claim.banner_alerts.filter(status="active") for alert in active_alerts: print(f"[{alert.intent}] {alert.narrative}") ``` ## When a claim is created A claim is created for an appointment or note only when the note's type is billable. A note type with `is_billable` set to `False`, such as the built-in Chart review type, produces a note and no claim, so check the flag rather than assuming every appointment produces a claim. The flag lives on [NoteType](/sdk/data-note/#notetype), reached through the note's `note_type_version` attribute. To read the claim itself, call the note's `get_claim()` method, which returns the note's most recent claim, or `None` when it has none: ```python from canvas_sdk.v1.data.note import Note note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") expects_claim = note.note_type_version.is_billable claim = note.get_claim() ``` ## Claim balances and totals The money on a claim comes from two stored balance fields plus a set of computed properties layered over the claim's line items and postings. `patient_balance` and `aggregate_coverage_balance` are stored on the claim and kept in sync by signals as coverage postings transfer money to the patient, as coverage is added or removed, and as the patient makes payments. Reading them needs no aggregation on your part, and they stay correct when coverage is added after a note is signed. The rest are computed on access, each over a different slice of the claim: Property| What it adds up ---|--- `total_charges`| `charge` across the claim's active line items, with copay and unlinked items excluded `total_paid`| `paid_amount` across every active posting on the claim, coverage and patient alike `total_payer_paid`| `paid_amount` across the active postings of the claim's active coverages `total_patient_paid`| `paid_amount` across the active postings on the claim's patient record `total_adjusted`| `contractual_adjusted_amount` plus `transferred_amount` across every active posting `balance`| `aggregate_coverage_balance` plus `patient_balance`, the coverage side and the patient side together A posting counts as active when it has not been entered in error. Because `total_paid` spans both sides of the claim, `total_payer_paid` and `total_patient_paid` are that figure split by who paid it. ### Computing patient-allocated charges To find how much has been allocated to the patient on a claim, add what the patient still owes to what they have already paid: ```python from canvas_sdk.v1.data.claim import Claim claim = Claim.objects.get(id="9d2e0f58-338b-11ec-8d3d-0242ac130003") patient_allocated = claim.patient_balance + claim.total_patient_paid ``` This holds for self-pay and insured claims alike, including a self-pay claim carrying no postings at all. ## Filtering ```python from canvas_sdk.v1.data.claim import Claim # Active claims only active_claims = Claim.objects.active() ``` ## Attributes ### Claim Represents a complete healthcare claim. Claim belongs to a Note and has a one-to-one relationship with a ClaimPatient. Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| note| [Note](/sdk/data-note/)| installment_plan| InstallmentPlan| current_queue| ClaimQueue| current_coverage| ClaimCoverage| accept_assign| Boolean| auto_accident| Boolean| auto_accident_state| String| employment_related| Boolean| other_accident| Boolean| accident_code| String| illness_date| Date| remote_batch_id| String| remote_file_id| String| prior_auth| String| narrative| String| account_number| String| snoozed_until| Date| patient_balance| Decimal| aggregate_coverage_balance| Decimal| created| DateTime| modified| DateTime| diagnosis_codes| ClaimDiagnosisCode[]| comments| ClaimComment[]| line_items| ClaimLineItem[]| labels| [TaskLabel](/sdk/data-task/#tasklabel)[]| metadata| ClaimMetadata[]| banner_alerts| ClaimBannerAlert[]| provider| ClaimProvider| incident_to| Boolean| supervising_provider| ClaimSupervisingProvider| latest_invoice| [Invoice](/sdk/data-invoice/#invoice)| patient| ClaimPatient| coverages| ClaimCoverage[]| submissions| ClaimSubmission[]| postings| [BasePosting](/sdk/data-posting/#baseposting)[]| total_charges| Decimal (computed)| Total charges for active line items total_paid| Decimal (computed)| Sum of paid amounts from postings total_adjusted| Decimal (computed)| Sum of adjustments and transfers balance| Decimal (computed)| Remaining balance (coverage plus patient) total_patient_paid| Decimal (computed)| Amount paid by the patient total_payer_paid| Decimal (computed)| Amount paid by coverages **Helpful Methods** : - `get_coverage_by_payer_id(payer_id: str, subscriber_number: str | None = None)`: Finds the active coverage associated with a payer_id. Optionally checks if the subscriber_number matches, which will choose the correct coverage in the case where a patient has two coverages with the same payer_id. ### ClaimSupervisingProvider An immutable snapshot of a claim's supervising provider (837P loop 2310D), captured at claim creation from the note's supervising provider and frozen thereafter, so later edits to the note or the underlying Staff record do not retroactively change a submitted claim. Field Name| Type ---|--- id| UUID dbid| Integer claim| Claim staff| [Staff](/sdk/data-staff/#staff) first_name| String last_name| String middle_name| String npi| String taxonomy| String tax_id| String tax_id_type| [TaxIDType](/sdk/data-enumeration-types/#taxidtype) created| DateTime modified| DateTime ### ClaimLineItem Represents individual billed procedures or services tied to a claim. Field Name| Type ---|--- id| UUID dbid| Integer billing_line_item| [BillingLineItem](/sdk/data-billing-line-item/) diagnosis_codes| ClaimLineItemDiagnosisCode[] modifiers| ClaimLineItemModifier[] claim| Claim status| ClaimLineItemStatus charge| Decimal from_date| String thru_date| String narrative| String ndc_code| String ndc_dosage| String ndc_measure| String place_of_service| [PracticeLocationPOS](/sdk/data-note/#practicelocationpos) proc_code| String display| String remote_chg_id| String units| Integer epsdt| String family_planning| FamilyPlanningOptions created| DateTime modified| DateTime newlineitempayments| QuerySet[[NewLineItemPayment](/sdk/data-posting/#newlineitempayment)] newlineitemadjustments| QuerySet[[NewLineItemAdjustment](/sdk/data-posting/#newlineitemadjustment)] lineitemtransfers| QuerySet[[LineItemTransfer](/sdk/data-posting/#lineitemtransfer)] ### ClaimLineItemDiagnosisCode Represents a diagnosis code for a given ClaimLineItem. There exists one ClaimLineItemDiagnosisCode for each ClaimDiagnosisCode, and the "linked" attribute indicates whether or not the diagnosis code is linked to the line item. Field Name| Type ---|--- id| UUID dbid| Integer line_item| ClaimLineItem claim_diagnosis_code| ClaimDiagnosisCode code| String poa| String linked| Boolean created| DateTime modified| DateTime ### ClaimLineItemModifier Represents a modifier code for a given ClaimLineItem. Field Name| Type ---|--- id| UUID dbid| Integer line_item| ClaimLineItem modifier| String created| DateTime modified| DateTime ### ClaimCoverage Links a claim to a specific insurance coverage. Field Name| Type ---|--- id| UUID dbid| Integer claim| Claim coverage| [Coverage](/sdk/data-coverage/) active| Boolean payer_name| String payer_id| String payer_typecode| String payer_order| ClaimPayerOrder payer_addr1| String payer_addr2| String payer_city| String payer_state| String payer_zip| String payer_plan_type| ClaimTypeCode coverage_type| [CoverageType](/sdk/data-coverage/#coveragetype) subscriber_employer| String subscriber_group| String subscriber_number| String subscriber_plan| String subscriber_dob| String subscriber_first_name| String subscriber_last_name| String subscriber_middle_name| String subscriber_phone| String subscriber_sex| [PersonSex](/sdk/data-patient/#sexatbirth) subscriber_addr1| String subscriber_addr2| String subscriber_city| String subscriber_state| String subscriber_zip| String subscriber_country| String patient_relationship_to_subscriber| [CoverageRelationshipCode](/sdk/data-coverage/#coveragerelationshipcode) pay_to_addr1| String pay_to_addr2| String pay_to_city| String pay_to_state| String pay_to_zip| String resubmission_code| String payer_icn| String created| DateTime modified| DateTime ### ClaimComment Represents a free-text comment made on a Claim. Field Name| Type ---|--- id| UUID dbid| Integer claim| Claim created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) comment| String ### ClaimBannerAlert Represents banner alerts associated with a claim. Banner alerts are displayed in the UI to surface important information about a claim. To create or remove `ClaimBannerAlert` records, see [Claim Effects](/sdk/effect-claims/#add-banner). Field Name| Type ---|--- id| UUID dbid| Integer claim| Claim plugin_name| String key| String narrative| String intent| [BannerAlertIntent](/sdk/data-banner-alert/#banneralertintent) href| String status| [BannerAlertStatus](/sdk/data-banner-alert/#banneralertstatus) created| DateTime modified| DateTime ### ClaimDiagnosisCode Represents diagnosis codes associated with a claim, ordered by rank. Field Name| Type ---|--- id| UUID dbid| Integer claim| Claim line_item_diagnosis_codes| ClaimLineItemDiagnosisCode[] rank| Integer code| String display| String created| DateTime modified| DateTime ### ClaimQueue Defines the metadata for claim queues used in revenue workflows. Field Name| Type ---|--- id| UUID dbid| Integer queue_sort_ordering| Integer name| String display_name| String description| String show_in_revenue| Boolean visible_columns| Array[ClaimQueueColumns] created| DateTime modified| DateTime ### ClaimPatient Captures patient-level data related to a specific claim. Field Name| Type ---|--- dbid| Integer claim| Claim photo| String dob| String first_name| String last_name| String middle_name| String phone| String sex| [PersonSex](/sdk/data-patient/#sexatbirth) ssn| String addr1| String addr2| String city| String state| String zip| String country| String created| DateTime modified| DateTime ### ClaimLabel Represents labels assigned to the claim. Field Name| Type ---|--- id| UUID dbid| Integer claim| Claim label| [TaskLabel](/sdk/data-task/#tasklabel) ### ClaimMetadata Represents key-value metadata associated with a claim. Each claim-key pair is unique. Field Name| Type ---|--- id| UUID dbid| Integer claim| Claim key| String value| String created| DateTime modified| DateTime ### ClaimProvider Captures provider-level data related to a specific claim. Field Name| Type ---|--- id| UUID dbid| Integer claim| Claim clia_number| String billing_provider_name| String billing_provider_phone| String billing_provider_addr1| String billing_provider_addr2| String billing_provider_city| String billing_provider_state| String billing_provider_zip| String billing_provider_id| String billing_provider_npi| String billing_provider_tax_id| String billing_provider_tax_id_type| String billing_provider_taxonomy| String provider_id| String provider_first_name| String provider_last_name| String provider_middle_name| String provider_npi| String provider_tax_id| String provider_tax_id_type| String provider_taxonomy| String provider_ptan_identifier| String provider_addr1| String provider_addr2| String provider_city| String provider_state| String provider_zip| String referring_provider_id| String referring_provider_first_name| String referring_provider_last_name| String referring_provider_middle_name| String referring_provider_npi| String referring_provider_ptan_identifier| String ordering_provider_first_name| String ordering_provider_last_name| String ordering_provider_middle_name| String ordering_provider_npi| String facility_id| String facility_name| String facility_npi| String facility_addr1| String facility_addr2| String facility_city| String facility_state| String facility_zip| String hosp_from_date| String hosp_to_date| String created| DateTime modified| DateTime ### ClaimSubmission Captures clearinghouse submission details about a claim. Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime claim| Claim coverage| ClaimCoverage clearinghouse_claim_id| String claim_index| Integer ### InstallmentPlan Represents a payment plan between a patient and provider. Field Name| Type ---|--- dbid| Integer creator| [CanvasUser](/sdk/data-canvasuser/) patient| [Patient](/sdk/data-patient/) total_amount| Decimal status| InstallmentPlanStatus expected_payoff_date| Date created| DateTime modified| DateTime claims| Claim[] ## Enumeration types ### ClaimLineItemStatus Value| Label ---|--- active| Active removed| Removed ### LineItemCodes Value --- COPAY UNLINKED ### FamilyPlanningOptions Value| Label ---|--- Y| Yes N| No ### ClaimLineItemStatus Value| Label ---|--- active| Active removed| Removed ### LineItemCodes Value --- COPAY UNLINKED ### FamilyPlanningOptions Value| Label ---|--- Y| Yes N| No ### ClaimPayerOrder Value| Label ---|--- Primary| Primary Secondary| Secondary Tertiary| Tertiary Quaternary| Quaternary Quinary| Quinary ### ClaimTypeCode Code| Description ---|--- 12| Working Aged (Age 65 or older) 13| End-Stage Renal Disease 14| No-fault 15| Workers Compensation 41| Black Lung 42| Veterans Administration 43| Disabled (Under Age 65) 47| Other Liability Insurance is primary ""| No Typecode necessary ### ClaimQueueColumns Value| Label ---|--- NoteType| Note type ClaimID| Claim ID DateOfService| Date of service Patient| Patient ActiveInsurance| Active insurance InsuranceBalance| Insurance balance PatientBalance| Patient balance DaysInQueue| Days in queue Provider| Provider Guarantor| Guarantor LatestRemit| Latest remit LastInvoiced| Last invoiced SnoozedUntil| Snoozed until Labels| Labels ### ClaimQueues Value| Label ---|--- 1| Appointment 2| NeedsClinicianReview 3| NeedsCodingReview 4| QueuedForSubmission 5| FiledAwaitingResponse 6| RejectedNeedsReview 7| AdjudicatedOpenBalance 8| PatientBalance 9| ZeroBalance 10| Trash ### InstallmentPlanStatus Value| Label ---|--- active| Active completed| Completed cancelled| Cancelled --- # Clipboard Source: https://docs.canvasmedical.com/sdk/data-clipboard/ ## Introduction The `Clipboard` model is the record of a Clipboard command. It holds free-text content staged on a patient's note, such as a pasted transcript or summary. A plugin queries it to read that content along with the patient and note the text belongs to. Clipboards are included regardless of command state (staged or committed); use `committed()` to filter to only committed commands. A plugin does not write clipboard content through this model. That content is recorded through the Clipboard command. ## Basic usage To get a clipboard by identifier, use the `get` method on the model manager: ```python from canvas_sdk.v1.data import Clipboard clipboard = Clipboard.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` The staged content is on the `text` attribute: ```python from canvas_sdk.v1.data import Clipboard clipboard = Clipboard.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") text = clipboard.text ``` `get`, `filter`, and `committed` are available on the manager. ## Accessing clipboards from a patient or note The clipboards for a patient or note are reachable through the `clipboards` reverse accessor, which is named the same on `Patient` and on `Note`: ```python from canvas_sdk.v1.data import Note, Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") patient_clipboards = patient.clipboards.all() note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") note_clipboards = note.clipboards.all() ``` ## Committed clipboards The `committed` method returns records whose underlying command has been committed and not entered in error: ```python from canvas_sdk.v1.data import Clipboard committed_clipboards = Clipboard.objects.committed() ``` ## Filtering Clipboards can be filtered by any attribute on the model with the `filter` method: ```python from canvas_sdk.v1.data import Clipboard clipboards = Clipboard.objects.filter(note__id="2c91b0d8-7b9d-4ef1-89e2-1f9a3a8a2b14") ``` ## Attributes ### Clipboard Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) text| String --- # Coding Gap Events Source: https://docs.canvasmedical.com/sdk/data-coding-gap-event/ ## Introduction The `CreateCodingGapEvent`, `ValidateCodingGapEvent`, `AssessCodingGapEvent`, and `DeferCodingGapEvent` models are records of the actions taken on a coding gap: create, validate, assess, and defer. Each model is the anchor for the corresponding coding gap command. A coding gap itself is represented by a [DetectedIssue](/sdk/data-detected-issue/) together with its `DetectedIssueEvidence`. These four models record only the actions taken on the coding gap. A coding gap event's `detected_issue` foreign key is nullable, so expect events without an associated `DetectedIssue`. These are audit records of create, validate, assess, and defer actions — useful for reporting, deduplication logic, and risk adjustment and HCC (Hierarchical Condition Category) coding gap analytics. A plugin does not perform these actions through these models. Each is performed instead through its own command — the Create Coding Gap, Validate Coding Gap, Assess Coding Gap, and Defer Coding Gap commands respectively. ## Basic usage To get a coding gap event by identifier, use the `get` method on the model manager. Each of the four models is imported from `canvas_sdk.v1.data`: ```python from canvas_sdk.v1.data import CreateCodingGapEvent create_event = CreateCodingGapEvent.objects.get(id="61a1853f-168f-4ed3-80d2-44e5d144bcf3") ``` All four models expose the same manager, so `get`, `filter`, and `committed` are available on each. ## Accessing events from a patient or note The coding gap events for a patient or note are reachable through reverse accessors. For each model, the accessor name is the same on `Patient` and on `Note`: Model| Patient & Note reverse accessor| DetectedIssue reverse accessor ---|---|--- CreateCodingGapEvent| `created_detected_issues`| `created_coding_gap_events` ValidateCodingGapEvent| `assessed_detected_issues`| `validated_coding_gap_events` AssessCodingGapEvent| `assessed_coding_gaps`| `assessed_coding_gap_events` DeferCodingGapEvent| `deferred_detected_issues`| `deferred_coding_gap_events` For a patient, use the accessor on the `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") create_events = patient.created_detected_issues.all() ``` The same records are reachable from the note they were recorded on, with the matching accessor on the `Note` object: ```python from canvas_sdk.v1.data.note import Note note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") defer_events = note.deferred_detected_issues.all() ``` ## Accessing events from a detected issue For a given coding gap, each model's events are reachable from the [DetectedIssue](/sdk/data-detected-issue/) through its own reverse accessor: Model| DetectedIssue reverse accessor ---|--- CreateCodingGapEvent| `created_coding_gap_events` ValidateCodingGapEvent| `validated_coding_gap_events` AssessCodingGapEvent| `assessed_coding_gap_events` DeferCodingGapEvent| `deferred_coding_gap_events` ```python from canvas_sdk.v1.data.detected_issue import DetectedIssue detected_issue = DetectedIssue.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") create_events = detected_issue.created_coding_gap_events.all() ``` ## Assessed coding gaps on a condition `AssessCodingGapEvent` records the conditions a coding gap was assessed against through its `conditions` many-to-many relationship: ```python from canvas_sdk.v1.data import AssessCodingGapEvent assess_event = AssessCodingGapEvent.objects.get(id="0b2f2f8c-3c2c-4d9e-9a1a-2b6a4c7d8e90") conditions = assess_event.conditions.all() ``` For a given condition, the assess events it was included in are reachable with the `assessed_coding_gaps` accessor on a `Condition` object: ```python from canvas_sdk.v1.data import Condition condition = Condition.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") assess_events = condition.assessed_coding_gaps.all() ``` ## Committed records The `committed` method returns records that have been committed and not entered in error: ```python from canvas_sdk.v1.data import CreateCodingGapEvent committed_create_events = CreateCodingGapEvent.objects.committed() ``` The `committed` method is available on all four managers. ## Attributes ### CreateCodingGapEvent Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) detected_issue| [DetectedIssue](/sdk/data-detected-issue/#detectedissue) ### ValidateCodingGapEvent Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) detected_issue| [DetectedIssue](/sdk/data-detected-issue/#detectedissue) ### AssessCodingGapEvent Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) detected_issue| [DetectedIssue](/sdk/data-detected-issue/#detectedissue) conditions| [Condition](/sdk/data-condition)[] ### DeferCodingGapEvent Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) detected_issue| [DetectedIssue](/sdk/data-detected-issue/#detectedissue) --- # CommandTypeSetting Source: https://docs.canvasmedical.com/sdk/data-command-type-setting/ ## Introduction The `CommandTypeSetting` model has one row for each command type available on the instance, with whether it is turned on. An administrator turns command types on and off in Canvas. Use it to leave out commands a provider cannot add, for example to skip suggesting a follow-up when the follow-up command is turned off. A command type that isn't available on the instance has no row. Treat a missing row the same as a command type that is turned off. ## Basic usage To check whether one command type is turned on, filter on its schema key and `active`: ```python from canvas_sdk.v1.data import CommandTypeSetting follow_up_on = CommandTypeSetting.objects.filter(schema_key="followUp", active=True).exists() if follow_up_on: ... ``` The check returns `False` both when the command type is turned off and when it has no row. A `get` call for a schema key with no row raises `CommandTypeSetting.DoesNotExist`. ## Filtering Command type settings can be filtered by any attribute that exists on the model. ### Active command types To get the schema keys of every command type turned on for the instance, in one query: ```python from canvas_sdk.v1.data import CommandTypeSetting active = set( CommandTypeSetting.objects.filter(active=True).values_list("schema_key", flat=True) ) ``` ## Attributes ### CommandTypeSetting Field Name| Type ---|--- dbid| Integer schema_key| String active| Boolean - **dbid** : The internal database primary key. - **schema_key** : The command type's schema key, the same value as [`Command.schema_key`](/sdk/data-command/#command) (e.g., `followUp`, `refer`, `prescribe`). - **active** : Whether the command type is turned on for the instance. --- # Command Source: https://docs.canvasmedical.com/sdk/data-command/ ## Introduction The `Command` model represents a [command](/sdk/commands/) in a note. Private Notes commands (`schema_key` of `privateNotes`) are not available through this model. Only the provider who writes a private note can see it, so `Command.objects` and `note.commands` never return these commands, even when you filter by their `schema_key` or `id`. ## Basic usage To get a command by identifier, use the `get` method on the `Command` model manager: ```python from canvas_sdk.v1.data.command import Command command = Command.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` ## Filtering Commands can be filtered by any attribute that exists on the model. Filtering for commands is done with the `filter` method on the `Command` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.command import Command commands = Command.objects.filter(state="committed") ``` ## Command types and data When events are fired as part of [Command Lifecycle Events](/sdk/events/#command-lifecycle-events), the `self.target` value that is available within a plugin will contain the `id` value of the command. For example: ```python from canvas_sdk.handlers import BaseHandler from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from logger import log class MyHandler(BaseHandler): RESPONDS_TO = [ EventType.Name(EventType.REASON_FOR_VISIT_COMMAND__POST_UPDATE), ] def compute(self) -> list[Effect]: log.info(self.target) # logs the Command id ``` Using this value, the `Command` model can be queried to fetch additional data about the command. Two main fields to pay attention to here are the `schema_key` and `data` fields. The `schema_key` field contains the type of the command, while the `data` field contains a JSON object with command data as key/value pairs: ```python import json from canvas_sdk.effects import Effect from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data.command import Command from logger import log class MyHandler(BaseHandler): def compute(self) -> list[Effect]: command_instance = Command.objects.get(id=self.target) log.info(command_instance.schema_key) log.info(json.dumps(command_instance.data, indent=2)) ``` For example, for a _Reason For Visit_ command, the preceding code would log the following lines: ```sh reasonForVisit { "coding": { "text": "Accident-prone", "extra": null, "value": "165002", "disabled": false, "annotations": null, "description": null }, "comment": "Patient would like to discuss condition." } ``` The following table shows the different command `schema_key` values with links to their respective [Command Modules](/sdk/commands). The attributes shown in each corresponding entry contain the structure that will appear in the `data` JSON field of each `Command`. Schema Key| Command Data ---|--- addCondition| [AddCondition](/sdk/commands/#addcondition) adjustPrescription| [AdjustPrescription](/sdk/commands/#adjustprescription) allergy| [Allergy](/sdk/commands/#allergy) assess| [Assess](/sdk/commands/#assess) changeMedication| [ChangeMedication](/sdk/commands/#changemedication) closeGoal| [CloseGoal](/sdk/commands/#closegoal) diagnose| [Diagnose](/sdk/commands/#diagnose) familyHistory| [FamilyHistory](/sdk/commands/#familyhistory) followUp| [FollowUp](/sdk/commands/#followup) goal| [Goal](/sdk/commands/#goal) hpi| [HistoryOfPresentIllness](/sdk/commands/#historyofpresentillness) imagingOrder| [ImagingOrder](/sdk/commands/#imagingorder) instruct| [Instruct](/sdk/commands/#instruct) labOrder| [LabOrder](/sdk/commands/#laborder) medicalHistory| [MedicalHistory](/sdk/commands/#medicalhistory) medicationStatement| [MedicationStatement](/sdk/commands/#medicationstatement) orderDme| [OrderDme](/sdk/commands/#orderdme) perform| [Perform](/sdk/commands/#perform) plan| [Plan](/sdk/commands/#plan) pocLabTest| [POCLabTest](/sdk/commands/#poclabtest) prescribe| [Prescribe](/sdk/commands/#prescribe) questionnaire| [Questionnaire](/sdk/commands/#questionnaire) reasonForVisit| [ReasonForVisit](/sdk/commands/#reasonforvisit) refer| [Refer](/sdk/commands/#refer) refill| [Refill](/sdk/commands/#refill) removeAllergy| [RemoveAllergy](/sdk/commands/#removeallergy) removePastMedicalHistory| [RemovePastMedicalHistory](/sdk/commands/#remove-past-medical-history) resolveCondition| [ResolveCondition](/sdk/commands/#resolve-condition) stopMedication| [StopMedication](/sdk/commands/#stopmedication) surgicalHistory| [SurgicalHistory](/sdk/commands/#surgicalhistory) task| [Task](/sdk/commands/#task) updateDiagnosis| [UpdateDiagnosis](/sdk/commands/#updatediagnosis) updateGoal| [UpdateGoal](/sdk/commands/#updategoal) vitals| [Vitals](/sdk/commands/#vitals) **PLEASE NOTE** the Commands Module is under development and Canvas is working to migrate all commands to be available. This means that some commands are not able to emit events available in plugins, and historical commands created prior to their Commands Module availability may not be able to be queried using the data module. [This product updates table](/product-updates/commands-module/) shows the commands and their release statuses. If a command in a chart is not available by querying the `Command` data model, the data is still available to be queried using corresponding data models (i.e. [Questionnaire](/sdk/data-questionnaire/), [ImagingOrder](/sdk/data-imaging/), etc.). ## The anchor object Most commands write a record of their own when they are entered: a Diagnose command creates a `Condition`, a Prescribe command creates a `Prescription`, and so on. That record is the command's anchor object, and `anchor_object` returns it as an instance of the matching data model: ```python from canvas_sdk.v1.data.command import Command command = Command.objects.get(id="c1b5a4d2-7e3f-4a8b-9c6d-2f1e0a9b8c7d") anchor = command.anchor_object ``` `anchor_object_type` and `anchor_object_dbid` identify the record, and `anchor_object` looks it up for you. It returns `None` when the command has no anchor recorded. `schema_key`| `anchor_object` returns ---|--- `addCondition`| [Condition](/sdk/data-condition/#condition) `adjustDiagnosis`| [Assessment](/sdk/data-assessment/#assessment) `adjustPrescription`| [Prescription](/sdk/data-prescription/#prescription) `adjustProtocol`| [ProtocolOverride](/sdk/data-protocol-override/#protocoloverride) `allergy`| [AllergyIntolerance](/sdk/data-allergy-intolerance/#allergyintolerance) `approveChange`| [PrescriptionChangeResponse](/sdk/data-prescription-change-response/#prescriptionchangeresponse) `approveRefill`| [Prescription](/sdk/data-prescription/#prescription) `assess`| [Assessment](/sdk/data-assessment/#assessment) `assessCodingGap`| [AssessCodingGapEvent](/sdk/data-coding-gap-event/#assesscodinggapevent) `cancelPrescription`| [CancelPrescription](/sdk/data-cancel-prescription/#cancelprescription) `changeMedication`| [ChangeMedication](/sdk/data-change-medication/#changemedication) `chartSectionReview`| [ChartSectionReview](/sdk/data-chart-section-review/#chartsectionreview) `clipboard`| [Clipboard](/sdk/data-clipboard/#clipboard) `closeGoal`| [UpdateGoal](/sdk/data-goal/#updategoal) `createCodingGap`| [CreateCodingGapEvent](/sdk/data-coding-gap-event/#createcodinggapevent) `deferCodingGap`| [DeferCodingGapEvent](/sdk/data-coding-gap-event/#defercodinggapevent) `denyChange`| [PrescriptionChangeResponse](/sdk/data-prescription-change-response/#prescriptionchangeresponse) `denyRefill`| [Prescription](/sdk/data-prescription/#prescription) `device`| [Device](/sdk/data-device/#device) `diagnose`| [Condition](/sdk/data-condition/#condition) `educationalMaterial`| [EducationalMaterial](/sdk/data-educational-material/#educationalmaterial) `exam`| [Interview](/sdk/data-questionnaire/#interview) `familyHistory`| [FamilyHistory](/sdk/data-family-history/#familyhistory) `followUp`| [FollowUp](/sdk/data-follow-up/#followup) `goal`| [Goal](/sdk/data-goal/#goal) `hpi`| [HistoryOfPresentIllness](/sdk/data-history-present-illness/#historyofpresentillness) `imagingOrder`| [ImagingOrder](/sdk/data-imaging/#imagingorder) `imagingReview`| [ImagingReview](/sdk/data-imaging/#imagingreview) `immunizationStatement`| [ImmunizationStatement](/sdk/data-immunization/#immunizationstatement) `immunize`| [Immunization](/sdk/data-immunization/#immunization) `instruct`| [Instruction](/sdk/data-instruction/#instruction) `labOrder`| [LabOrder](/sdk/data-labs/#laborder) `labReview`| [LabReview](/sdk/data-labs/#labreview) `medicalHistory`| [Condition](/sdk/data-condition/#condition) `medicationStatement`| [MedicationStatement](/sdk/data-medication-statement/#medicationstatement) `orderDme`| [DmeOrder](/sdk/data-dme/#dmeorder) `perform`| [Procedure](/sdk/data-procedure/#procedure) `plan`| [Plan](/sdk/data-plan/#plan) `pocLabTest`| [LabReport](/sdk/data-labs/#labreport) `prescribe`| [Prescription](/sdk/data-prescription/#prescription) `questionnaire`| [Interview](/sdk/data-questionnaire/#interview) `reasonForVisit`| [ReasonForVisit](/sdk/data-reason-for-visit/#reasonforvisit) `refer`| [Referral](/sdk/data-referral/#referral) `reference`| [Reference](/sdk/data-reference/#reference) `referralReview`| [ReferralReview](/sdk/data-referral/#referralreview) `refill`| [Prescription](/sdk/data-prescription/#prescription) `removeAllergy`| [RemoveAllergyEvent](/sdk/data-remove-allergy-event/#removeallergyevent) `removePastMedicalHistory`| [RemovePastMedicalHistoryEvent](/sdk/data-remove-past-medical-history-event/#removepastmedicalhistoryevent) `resolveCondition`| [ResolveConditionEvent](/sdk/data-resolve-condition-event/#resolveconditionevent) `ros`| [Interview](/sdk/data-questionnaire/#interview) `snoozeProtocol`| [ProtocolOverride](/sdk/data-protocol-override/#protocoloverride) `stopMedication`| [StopMedicationEvent](/sdk/data-stop-medication-event/#stopmedicationevent) `structuredAssessment`| [Interview](/sdk/data-questionnaire/#interview) `surgicalHistory`| [Condition](/sdk/data-condition/#condition) `task`| [NoteTask](/sdk/data-task/#notetask) `updateDiagnosis`| [Condition](/sdk/data-condition/#condition) `updateGoal`| [UpdateGoal](/sdk/data-goal/#updategoal) `validateCodingGap`| [ValidateCodingGapEvent](/sdk/data-coding-gap-event/#validatecodinggapevent) `visualExamFinding`| [VisualExamFinding](/sdk/data-visual-exam-finding/#visualexamfinding) `vitals`| [VitalSignReading](/sdk/data-vital-sign-reading/#vitalsignreading) Custom commands| [CustomCommand](/sdk/data-custom-command/#customcommand) > **Warning:** `anchor_object` does not yet support the `uncategorizedDocumentReview` command, and reading it raises `LookupError`. Support is coming in an upcoming release. Until then, look the record up directly as shown below. The `uncategorizedDocumentReview` anchor is available as [UncategorizedClinicalDocumentReview](/sdk/data-uncategorized-clinical-document/#uncategorizedclinicaldocumentreview); `anchor_object` cannot find it because the model goes by a different name. Look it up from `anchor_object_dbid` instead: ```python from canvas_sdk.v1.data import UncategorizedClinicalDocumentReview from canvas_sdk.v1.data.command import Command command = Command.objects.get(id="c1b5a4d2-7e3f-4a8b-9c6d-2f1e0a9b8c7d") review = UncategorizedClinicalDocumentReview.objects.get(dbid=command.anchor_object_dbid) ``` If a command's anchor record has been deleted, `anchor_object` raises the data model's `DoesNotExist` rather than returning `None`. ## Attributes ### Command Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) state| String patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note/#note) schema_key| String data| JSON origination_source| String custom_html| String (optional) anchor_object_type| String anchor_object_dbid| Integer anchor_object| Model (optional) metadata| QuerySet[[CommandMetadata](/sdk/data-command/#commandmetadata)] The `custom_html` field stores HTML content that is rendered alongside the command in the note. This field is optional and defaults to `None`. Use the [`set_custom_html`](/sdk/commands/#set_custom_html) method to set or clear this field on a staged command. ### CommandMetadata `CommandMetadata` stores custom key-value pairs associated with a command. Metadata can be upserted using the `upsert_metadata` method on any command effect class — see [CommandMetadata Effect](/sdk/effect-command-metadata/) for full details. ```python from canvas_sdk.v1.data.command import CommandMetadata # Get all metadata for a command metadata_entries = CommandMetadata.objects.filter(command__id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") # Get a specific metadata value entry = CommandMetadata.objects.get(command__id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35", key="my_plugin:priority") print(entry.value) ``` Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime command| [Command](/sdk/data-command/#command) key| String value| String --- # CompoundMedication Source: https://docs.canvasmedical.com/sdk/data-compound-medication/ ## Introduction The `CompoundMedication` model represents a compound medication formulation that can be prescribed to patients. Compound medications are customized medications mixed or prepared by a compounding pharmacy according to a prescription. ## Basic usage To get a compound medication by identifier, use the `get` method on the `CompoundMedication` model manager: ```python from canvas_sdk.v1.data.compound_medication import CompoundMedication compound_medication = CompoundMedication.objects.get(id="123") ``` ## Filtering Compound medications can be filtered by any attribute that exists on the model. Filtering for compound medications is done with the `filter` method on the `CompoundMedication` model manager. ### By attribute Specify attributes with `filter` to filter by those attributes: ```python from canvas_sdk.v1.data.compound_medication import CompoundMedication # Get all active compound medications active_medications = CompoundMedication.objects.filter(active=True) # Get compound medications by formulation compound_medications = CompoundMedication.objects.filter(formulation="Testosterone 200mg/mL in Grapeseed Oil") # Get all Schedule II controlled substances schedule_ii_medications = CompoundMedication.objects.filter(controlled_substance="II") # Get compound medications by potency unit tablet_medications = CompoundMedication.objects.filter(potency_unit_code="C48542") ``` ### Multiple filters You can combine multiple filters: ```python from canvas_sdk.v1.data.compound_medication import CompoundMedication # Get active compound medications that are controlled substances controlled_active = CompoundMedication.objects.filter( active=True, controlled_substance__in=["II", "III", "IV", "V"] ) ``` ## Attributes ### CompoundMedication Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime id| UUID active| Boolean formulation| String potency_unit_code| PotencyUnit controlled_substance| ControlledSubstanceSchedule controlled_substance_ndc| String compound_medication| QuerySet[[Prescription](/sdk/data-prescription/#prescription)] ## Enumeration types ### PotencyUnit Value| Label ---|--- C62412| Applicator C54564| Blister C64696| Caplet C48480| Capsule C64933| Each C53499| Film C48155| Gram C69124| Gum C48499| Implant C62276| Insert C48504| Kit C120263| Lancet C48506| Lozenge C28254| Milliliter C48521| Packet C65032| Pad C48524| Patch C120216| Pen Needle C62609| Ring C53502| Sponge C53503| Stick C48538| Strip C48539| Suppository C53504| Swab C48542| Tablet C48548| Troche C38046| Unspecified C48552| Wafer ### ControlledSubstanceSchedule Key| Value| Label ---|---|--- SCHEDULE_NOT_SCHEDULED| N| None SCHEDULE_II| II| Schedule II SCHEDULE_III| III| Schedule III SCHEDULE_IV| IV| Schedule IV SCHEDULE_V| V| Schedule V ## Notes - The `formulation` field has a maximum length of 105 characters (as defined by Surescripts). --- # Condition Source: https://docs.canvasmedical.com/sdk/data-condition/ ## Introduction The `Condition` model represents a clinical condition, problem, diagnosis, or other event, situation, issue, or clinical concept that has risen to a level of concern. ## Basic usage To get a condition by identifier, use the `get` method on the `Condition` model manager: ```python from canvas_sdk.v1.data.condition import Condition condition = Condition.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the conditions for a patient can be accessed with the `conditions` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") conditions = patient.conditions.all() ``` If you have a patient ID, you can get the conditions for the patient with the `for_patient` method on the `Condition` model manager: ```python from canvas_sdk.v1.data.condition import Condition patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" condition = Condition.objects.for_patient(patient_id) ``` ## Codings The codings for a condition can be accessed with the `codings` attribute on an `Condition` object: ```python from canvas_sdk.v1.data.condition import Condition from logger import log condition = Condition.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") for coding in condition.codings.all(): log.info(f"system: {coding.system}") log.info(f"code: {coding.code}") log.info(f"display: {coding.display}") ``` ## Filtering Conditions can be filtered by any attribute that exists on the model. Filtering for conditions is done with the `filter` method on the `Condition` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.condition import Condition conditions = Condition.objects.filter(onset_date__gte="2024-10-15") ``` ### By ValueSet See [Value Sets](/sdk/data-value-sets/) for the library of built-in value sets and how to create your own. Filtering by ValueSet works a little differently. The `find` method on the model manager is used to perform `ValueSet` filtering: ```python from canvas_sdk.v1.data.condition import Condition from canvas_sdk.value_set.v2022.condition import Diabetes conditions = Condition.objects.find(Diabetes) ``` ## Attributes ### Condition Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime entered_in_error| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) onset_date| Date resolution_date| Date clinical_status| ClinicalStatus codings| ConditionCoding[] lab_order_reason_conditions| [LabOrderReasonConditionCoding](/sdk/data-labs/#laborderreasoncondition)[] notes| String surgical| Boolean assessed_coding_gaps| [AssessCodingGapEvent](/sdk/data-coding-gap-event/#assesscodinggapevent)[] assessments| [Assessment](/sdk/data-assessment/#assessment)[] resolutions| [ResolveConditionEvent](/sdk/data-resolve-condition-event/#resolveconditionevent)[] past_medical_history_removals| [RemovePastMedicalHistoryEvent](/sdk/data-remove-past-medical-history-event/#removepastmedicalhistoryevent)[] ### ConditionCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean condition| Condition ## Enumeration types ### ClinicalStatus Value| Label ---|--- active| active relapse| relapse remission| remission resolved| resolved investigative| investigative --- # ContentType Source: https://docs.canvasmedical.com/sdk/data-content-type/ ## Introduction The `ContentType` model exposes Django content types. Use it to resolve the content type id for a given model, which is required when working with generic relations (such as [document references](/sdk/data-document-reference)) and when generating permalinks. A content type is identified by two **stable** values — `app_label` and `model` — that are the same on every Canvas instance. Its `dbid` (the content type id) is a per-database auto-increment that **is not stable across environments**. Always resolve the `dbid` at runtime from the `app_label` and `model`; never hardcode a content type id, or it will point at the wrong model in another environment. ## Basic usage To get a content type by its database id, use the `get` method on the `ContentType` model manager: ```python from canvas_sdk.v1.data import ContentType content_type = ContentType.objects.get(dbid=42) ``` ## Resolving a content type at runtime Because the `dbid` differs per environment, look the content type up by its stable `app_label` and `model`, then read `dbid` from the result: ```python from canvas_sdk.v1.data import ContentType content_type = ContentType.objects.filter(app_label="api", model="note").first() if content_type: # Resolved for this environment — safe to use for a generic relation or permalink. content_type_id = content_type.dbid ``` ## Filtering Content types can be filtered by any attribute that exists on the model. Filtering for content types is done with the `filter` method on the `ContentType` model manager. ### By model To find the content type for a specific model, filter by `app_label` and `model`: ```python from canvas_sdk.v1.data import ContentType content_type = ContentType.objects.filter(app_label="api", model="note").first() if content_type: print(f"Content type id: {content_type.dbid}") ``` ## app_label and model for data module models Use these stable values to resolve a content type with `ContentType.objects.filter(app_label=..., model=...)`. The `model` value is the lowercased Django model name, and most data module models live under the `api` app. This list is not exhaustive — any model not shown here can be resolved the same way once you know its `app_label` and `model`. ### `api` app SDK data model| app_label| model ---|---|--- [AllergyIntolerance](/sdk/data-allergy-intolerance/)| `api`| `allergyintolerance` [Appointment](/sdk/data-appointment/)| `api`| `appointment` [Assessment](/sdk/data-assessment/)| `api`| `assessment` [BannerAlert](/sdk/data-banner-alert/)| `api`| `banneralert` [ChartSectionReview](/sdk/data-chart-section-review/)| `api`| `chartsectionreview` [Condition](/sdk/data-condition/)| `api`| `condition` [Coverage](/sdk/data-coverage/)| `api`| `coverage` [DetectedIssue](/sdk/data-detected-issue/)| `api`| `detectedissue` [Device](/sdk/data-device/)| `api`| `device` [DiagnosticReport](/sdk/data-labs/#diagnosticreport)| `api`| `diagnosticreport` [DocumentReference](/sdk/data-document-reference/)| `api`| `documentreference` [Encounter](/sdk/data-encounter/)| `api`| `encounter` [Facility](/sdk/data-facility/)| `api`| `facility` [Goal](/sdk/data-goal/)| `api`| `goal` [ImagingOrder](/sdk/data-imaging/)| `api`| `imagingorder` [ImagingReport](/sdk/data-imaging/)| `api`| `imagingreport` [ImagingReview](/sdk/data-imaging/)| `api`| `imagingreview` [Immunization](/sdk/data-immunization/)| `api`| `immunization` [Instruction](/sdk/data-instruction/)| `api`| `instruction` [Interview](/sdk/data-questionnaire/)| `api`| `interview` [LabOrder](/sdk/data-labs/)| `api`| `laborder` [LabReport](/sdk/data-labs/)| `api`| `labreport` [LabValue](/sdk/data-labs/)| `api`| `labvalue` [Letter](/sdk/data-letter/)| `api`| `letter` [Medication](/sdk/data-medication/)| `api`| `medication` [MedicationStatement](/sdk/data-medication-statement/)| `api`| `medicationstatement` [Message](/sdk/data-message/)| `api`| `message` [Note](/sdk/data-note/)| `api`| `note` [Observation](/sdk/data-observation/)| `api`| `observation` [Organization](/sdk/data-organization/)| `api`| `organization` [OrganizationalEntity](/sdk/data-organizational-entity/)| `api`| `organizationalentity` [Patient](/sdk/data-patient/)| `api`| `patient` [PatientConsent](/sdk/data-patient-consent/)| `api`| `patientconsent` [PatientGroup](/sdk/data-patient-group/)| `api`| `patientgroup` [PracticeLocation](/sdk/data-practicelocation/)| `api`| `practicelocation` [Prescription](/sdk/data-prescription/)| `api`| `prescription` [Questionnaire](/sdk/data-questionnaire/)| `api`| `questionnaire` [ReasonForVisit](/sdk/data-reason-for-visit/)| `api`| `reasonforvisit` [Referral](/sdk/data-referral/)| `api`| `referral` [Staff](/sdk/data-staff/)| `api`| `staff` [StopMedicationEvent](/sdk/data-stop-medication-event/)| `api`| `stopmedicationevent` [Task](/sdk/data-task/)| `api`| `task` [Team](/sdk/data-team/)| `api`| `team` [UncategorizedClinicalDocument](/sdk/data-uncategorized-clinical-document/)| `api`| `uncategorizedclinicaldocument` [VisualExamFinding](/sdk/data-visual-exam-finding/)| `api`| `visualexamfinding` ### Other apps Some models live in a different Django app, so their `app_label` is not `api`: SDK data model| app_label| model ---|---|--- [Command](/sdk/data-command/)| `commands`| `command` [Application](/sdk/data-application/)| `plugin_io`| `application` [PluginCommand](/sdk/data-plugin-command/)| `plugin_io`| `plugincommand` [Calendar](/sdk/data-calendar/)| `calendars`| `calendar` [ExternalEvent](/sdk/data-external-event/)| `data_integration`| `externalevent` [ServiceProvider](/sdk/data-serviceprovider/)| `data_integration`| `serviceprovider` [ChargeDescriptionMaster](/sdk/data-charge-description-master/)| `quality_and_revenue`| `chargedescriptionmaster` [Claim](/sdk/data-claim/)| `quality_and_revenue`| `claim` [PayorSpecificCharge](/sdk/data-payor-specific-charge/)| `quality_and_revenue`| `payorspecificcharge` ## Attributes ### ContentType Field Name| Type ---|--- dbid| Integer app_label| String model| String - **dbid** : The internal database primary key, which is the content type id used for generic relations and permalinks. This value is environment-specific — resolve it at runtime rather than hardcoding it. - **app_label** : The label of the application the model belongs to (e.g., `api`). - **model** : The lowercased name of the model (e.g., `note`). --- # Coverage Source: https://docs.canvasmedical.com/sdk/data-coverage/ ## Introduction The `Coverage` model represents insurance coverage linked to [Patients](/sdk/data-patient/#patient). Coverages are linked to [Patient](/sdk/data-patient/#patient) instances, as well as `Transactor` instances, which represent the issuer for the corresponding coverage. `Coverage`s also have an associated `EligibilitySummary`, which provides the most up-to-date copay and coinsurance values. To create or change coverages from a plugin, use the [Coverage effect](/sdk/effect-coverage/). Coverages can also be linked to a [`Snapshot`](/sdk/data-snapshot/#snapshot), which provides access to insurance card images. Card images are captured via the Canvas iOS application, uploaded through the coverages modal, or attached by a plugin with the [Coverage effect](/sdk/effect-coverage/#card-images). ## Usage The `Coverage` model can be used to find all of the coverages defined in a Canvas instance, whether overall or for a particular patient. For example, to find all of the current coverages for a patient, the `Patient.coverages` method can be used: ```python >>> import arrow >>> from canvas_sdk.v1.data.patient import Patient >>> patient_1 = Patient.objects.get(id="a74592ae8a6c4d0ebe0799d3fb3713d1") >>> patient_1_current_coverages = patient_1.coverages.filter(coverage_end_date__gt=arrow.now().date().isoformat()) >>> print([coverage.issuer.name for coverage in patient_1_current_coverages]) ['AVALON HEALTHCARE SOLUTIONS CAPITAL BLUE CROSS'] ``` Alternatively, to find all of the `Coverage` instances issed by a particular issuer/transactor, the `Transactor` model can be queried: ```python >>> from canvas_sdk.v1.data.coverage import Coverage, Transactor >>> transactor_1 = Transactor.objects.get(payer_id="AVA03") >>> transactor_coverages = Coverage.objects.filter(issuer=transactor_1) >>> print(transactor_coverages) , ]> >>> ``` Find the latest eligibility summary for a patient: ```python from canvas_sdk.v1.data.coverage import Coverage, EligibilitySummary coverage = Coverage.objects.get(id="a74592ae-8a6c-4d0e-be07-99d3fb3713d1") elig_summary_from_model = EligibilitySummary.objects.filter(coverage=coverage).first() elig_summary_from_cvg = coverage.eligibility_summary if elig_summary_from_model: print(elig_summary_from_model.copay_cents, elig_summary_from_model.coinsurance) # 1000 5 if elig_summary_from_cvg: print(elig_summary_from_cvg.copay_cents, elig_summary_from_cvg.coinsurance) # 1000 5 ``` Access insurance card images through the coverage's snapshot: ```python from canvas_sdk.v1.data.coverage import Coverage coverage = Coverage.objects.get(id="a74592ae-8a6c-4d0e-be07-99d3fb3713d1") if coverage.snapshot: for image in coverage.snapshot.images.all(): print(image.image_url) # Presigned S3 URL for the insurance card image ``` ## Eligibility status `Coverage.eligibility_status` returns the [`EligibilityResponseStatus`](/sdk/data-eligibility-response/#eligibilityresponsestatus) of the coverage's most recent [`EligibilityResponse`](/sdk/data-eligibility-response/#eligibilityresponse). It returns `UNKNOWN` when the coverage has never been checked (it has no eligibility responses): ```python from canvas_sdk.v1.data.coverage import Coverage from canvas_sdk.v1.data.eligibility_response import EligibilityResponseStatus coverage = Coverage.objects.get(id="a74592ae-8a6c-4d0e-be07-99d3fb3713d1") if coverage.eligibility_status == EligibilityResponseStatus.ACTIVE: print("Coverage is active") ``` `Coverage.eligibility_status` returns `NOT_APPLICABLE` for a self-pay coverage, meaning one whose issuer has a `payer_id` of `PATIENT`. It resolves this case before consulting the stored eligibility responses, so a stale `FAILED` response left on a self-pay coverage is never surfaced. That is also what `Transactor.supports_eligibility_check` reports: it is `False` for the self-pay payer, whose `payer_id` is `PATIENT`, and `True` for every other issuer. Because it is computed on each access rather than stored, `eligibility_status` cannot be used in `filter()`. Filter on the coverage's [eligibility responses](/sdk/data-eligibility-response/#eligibilityresponse) instead, or read the property once you have the coverage in hand. A single [`EligibilityResponse.status`](/sdk/data-eligibility-response/#eligibilityresponse), by contrast, never resolves to `UNKNOWN` — that value belongs to the coverage, which has no response to defer to. To react to eligibility changes rather than poll for them, subscribe to the [eligibility response events](/sdk/events/#eligibility-responses). Those fire only when a response is saved, so a coverage that has never been checked emits no event at all: a plugin that has to catch never-verified coverages should read `eligibility_status` rather than rely on the events alone. ## Filtering The `filter` method can be used to filter by desired attributes. The following examples show commonly used operations to filter coverage data: **Show a Patient's Coverages in order of Rank (Primary, Secondary, etc.)** ```python >>> from canvas_sdk.v1.data.patient import Patient >>> patient_1 = Patient.objects.get(id="aebe4d3f5d18410388dc69c4b5169fc3") >>> patient_coverages = patient_1.coverages.all().order_by("coverage_rank") >>> print([(coverage.issuer.name, coverage.coverage_rank,) for coverage in patient_coverages]) [('AVALON HEALTHCARE SOLUTIONS CAPITAL BLUE CROSS', 1), ('Blue Cross Blue Shield of Arizona Advantage', 2)] ``` **Find All Expired Coverages** ```python >>> import arrow >>> from canvas_sdk.v1.data.coverage import Coverage >>> expired_coverages = Coverage.objects.filter(coverage_end_date__lt=arrow.now().date().isoformat()) >>> print([f"{coverage.issuer.name} expired {coverage.coverage_end_date.isoformat()}" for coverage in expired_coverages]) ['Blue Cross Blue Shield of Arizona Advantage expired 2025-01-10'] ``` ## Attributes ### Coverage Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime patient| [Patient](/sdk/data-patient/#patient) guarantor| [Patient](/sdk/data-patient/#patient) subscriber| [Patient](/sdk/data-patient/#patient) subscriber_identifier| String patient_relationship_to_subscriber| CoverageRelationshipCode issuer| Transactor id_number| String plan| String sub_plan| String group| String sub_group| String employer| String coverage_start_date| Date coverage_end_date| Date coverage_rank| CoverageRank state| CoverageState plan_type| CoverageType coverage_type| TransactorCoverageType issuer_address| TransactorAddress issuer_phone| TransactorPhone comments| Text stack| CoverageStack snapshot| [Snapshot](/sdk/data-snapshot/#snapshot) eligibility_summary| EligibilitySummary eligibility_status| [EligibilityResponseStatus](/sdk/data-eligibility-response/#eligibilityresponsestatus) (computed) claim_coverages| [ClaimCoverage](/sdk/data-claim/#claimcoverage)[] requests| [EligibilityRequest](/sdk/data-eligibility-response/#eligibilityrequest)[] eligibility_responses| [EligibilityResponse](/sdk/data-eligibility-response/#eligibilityresponse)[] ### Transactor Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime payer_id| String name| String type| String transactor_type| TransactorType clearinghouse_payer| Boolean institutional| Boolean institutional_enrollment_req| Boolean professional| Boolean professional_enrollment_req| Boolean era| Boolean era_enrollment_req| Boolean eligibility| Boolean eligibility_enrollment_req| Boolean workers_comp| Boolean secondary_support| Boolean claim_fee| Boolean remit_fee| Boolean state| String description| String active| Boolean use_provider_for_eligibility| Boolean supports_eligibility_check| Boolean (computed) use_for_submission| Transactor used_for_submission_by| Transactor[] coverage_types| TransactorCoverageType[] vaccines| [Vaccine](/sdk/data-vaccine/#vaccine)[] addresses| TransactorAddress[] coverages| Coverage[] phones| TransactorPhone[] specific_charges| [PayorSpecificCharge](/sdk/data-payor-specific-charge/#payorspecificcharge)[] remits| [BaseRemittanceAdvice](/sdk/data-posting/#baseremittanceadvice)[] ### TransactorAddress Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime line1| String line2| String city| String district| String state_code| String postal_code| String use| [AddressUse](/sdk/data-enumeration-types/#addressuse) type| [AddressType](/sdk/data-enumeration-types/#addresstype) longitude| Float latitude| Float start| Date end| Date country| String state| [AddressState](/sdk/data-enumeration-types/#addressstate) transactor| Transactor coverages| [Coverage](/sdk/data-coverage/#coverage)[] ### TransactorPhone Field Name| Type ---|--- id| UUIDField dbid| Integer created| DateTime modified| DateTime system| String value| String use| [ContactPointUse](/sdk/data-enumeration-types/#contactpointuse) use_notes| String rank| Integer state| [ContactPointState](/sdk/data-enumeration-types/#contactpointstate) transactor| Transactor coverages| [Coverage](/sdk/data-coverage/#coverage)[] ### EligibilitySummary Field Name| Type ---|--- id| UUIDField dbid| Integer created| DateTime modified| DateTime coverage| [Coverage](/sdk/data-coverage/#coverage) copay_cents| Integer coinsurance| Integer ## Enumeration types ### CoverageRank Value| Name| Label ---|---|--- 1| PRIMARY| Primary 2| SECONDARY| Secondary 3| TERTIARY| Tertiary 4| QUATERNARY| Quaternary 5| QUINARY| Quinary ### CoverageStack Value| Label ---|--- IN_USE| In use OTHER| Other REMOVED| Removed ### CoverageState Value| Label ---|--- active| Active deleted| Deleted ### CoverageType Value| Label ---|--- commercial| Commercial workerscomp| Workers Comp bcbs| Blue Cross Blue Shield champus| Tricare/Champus medicaid| Medicaid medicare| Medicare other| Other tpa| Third Party Administrator motorvehicle| Motor Vehicle lien| Attorney/Lien pip| Personal Injury ### CoverageRelationshipCode Value| Label ---|--- 18| Self 01| Spouse 19| Natural Child, insured has financial responsibility 43| Natural Child, insured does not have financial responsibility 17| Step Child 10| Foster Child 15| Ward of the Court 20| Employee 21| Unknown 22| Handicapped Dependent 39| Organ donor 40| Cadaver donor 05| Grandchild 07| Niece/Nephew 41| Injured Plaintiff 23| Sponsored Dependent 24| Minor Dependent of a Minor Dependent 32| Mother 33| Father 04| Grandparent 53| Life Partner 29| Significant Other G8| Other ### TransactorCoverageType Value| Label ---|--- ANNU| annuity policy AUTOPOL| automobile CHAR| charity program COL| collision coverage policy CRIME| crime victim program DENTAL| dental care policy DENTPRG| dental program DIS| disability insurance policy DISEASE| disease specific policy DRUGPOL| drug policy EAP| employee assistance program EWB| employee welfare benefit plan policy ENDRENAL| end renal program EHCPOL| extended healthcare FLEXP| flexible benefit plan policy GOVEMP| government employee health program HIP| health insurance plan policy HMO| health maintenance organization policy HSAPOL| health spending account HIRISK| high risk pool program HIVAIDS| HIV-AIDS program IND| indigenous peoples health program LIFE| life insurance policy LTC| long term care policy MCPOL| managed care policy MANDPOL| mandatory health program MENTPOL| mental health policy MENTPRG| mental health program MILITARY| military health program pay| Pay POS| point of service policy PPO| preferred provider organization policy PNC| property and casualty insurance policy DISEASEPRG| public health program PUBLICPOL| public healthcare REI| reinsurance policy RETIRE| retiree health program SAFNET| safety net clinic program SOCIAL| social service program SUBSIDIZ| subsidized health program SUBSIDMC| subsidized managed care program SUBSUPP| subsidized supplemental health program SUBPOL| substance use policy SUBPRG| substance use program SURPL| surplus line insurance policy TLIFE| term life insurance policy UMBRL| umbrella liability insurance policy UNINSMOT| uninsured motorist policy ULIFE| universal life insurance policy VET| veteran health program VISPOL| vision care policy CANPRG| women's cancer detection program WCBPOL| worker's compensation ### TransactorType Value| Label ---|--- commercial| Commercial workerscomp| Workers Comp champus| Tricare/Champus medicaid| Medicaid medicare| Medicare medicare_advantage| Medicare Advantage CHIP| CHIP automobile| Automobile employer| Employer direct_care| Direct Care bcbs| Blue Cross Blue Shield --- # CustomCommand Source: https://docs.canvasmedical.com/sdk/data-custom-command/ ## Introduction The `CustomCommand` model is the anchor for a [custom command](/sdk/commands-custom-command/): the plugin-defined, read-only HTML content a plugin inserts into a note. Use this data model to read back the custom commands recorded on a chart. To insert one, use the [Custom Command](/sdk/commands-custom-command/) command class instead. ## Basic usage To get a custom command by identifier, use the `get` method on the `CustomCommand` model manager: ```python from canvas_sdk.v1.data import CustomCommand custom_command = CustomCommand.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the custom commands for a patient can be accessed with the `custom_commands` attribute: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") custom_commands = patient.custom_commands.all() ``` The custom commands recorded on a note are reachable the same way, through the note's `custom_commands` attribute: ```python from canvas_sdk.v1.data.note import Note note = Note.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") custom_commands = note.custom_commands.all() ``` ## Committed records The `committed` method returns custom commands that have been committed and not entered in error: ```python from canvas_sdk.v1.data import CustomCommand committed_commands = CustomCommand.objects.committed() ``` ## Resolving the plugin registration The `plugin_command` property returns the [`PluginCommand`](/sdk/data-plugin-command/) registration the command was created from, resolved through the anchoring command's `schema_key`. It is `None` when the anchor has no command, or when its `schema_key` is not a registered plugin command: ```python custom_command = CustomCommand.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") plugin_command = custom_command.plugin_command if plugin_command: print(plugin_command.label) ``` ## Reading the command data The `data` property returns the anchoring [`Command`](/sdk/data-command/)'s `data`, a dictionary of the command's stored field values. It is `None` when there is no anchoring command: ```python custom_command = CustomCommand.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") data = custom_command.data ``` ## Attributes ### CustomCommand Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) plugin_command| [PluginCommand](/sdk/data-plugin-command/) data| JSON --- # DetectedIssue Source: https://docs.canvasmedical.com/sdk/data-detected-issue/ ## Introduction The `DetectedIssue` model represents an actual or potential clinical issue with or between one or more active or proposed clinical actions for a patient. ## Basic usage To get a detected issue by identifier, use the `get` method on the `DetectedIssue` model manager: ```python from canvas_sdk.v1.data.detected_issue import DetectedIssue detected_issue = DetectedIssue.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the detected issues for a patient can be accessed with the `detected_issues` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") detected_issues = patient.detected_issues.all() ``` ## Evidence The codings for the evidence of a detected issue can be accessed with the `evidence` attribute on a `DetectedIssue` object: ```python from canvas_sdk.v1.data.detected_issue import DetectedIssue from logger import log detected_issue = DetectedIssue.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") for coding in detected_issue.evidence.all(): log.info(f"system: {coding.system}") log.info(f"code: {coding.code}") log.info(f"display: {coding.display}") ``` ## Filtering Detected issues can be filtered by any attribute that exists on the model. Filtering for detected issues is done with the `filter` method on the `DetectedIssue` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.detected_issue import DetectedIssue detected_issues = DetectedIssue.objects.filter(status="active") ``` ### Committed detected issues The `committed` method returns detected issues that have been committed and not entered in error: ```python from canvas_sdk.v1.data.detected_issue import DetectedIssue committed_detected_issues = DetectedIssue.objects.committed() ``` ## Attributes ### DetectedIssue Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime identified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) code| String status| DetectedIssueStatus severity| DetectedIssueSeverity reference| String issue_identifier| String issue_identifier_system| String detail| String evidence| DetectedIssueEvidence[] assessed_coding_gap_events| [AssessCodingGapEvent](/sdk/data-coding-gap-event/#assesscodinggapevent)[] created_coding_gap_events| [CreateCodingGapEvent](/sdk/data-coding-gap-event/#createcodinggapevent)[] deferred_coding_gap_events| [DeferCodingGapEvent](/sdk/data-coding-gap-event/#defercodinggapevent)[] validated_coding_gap_events| [ValidateCodingGapEvent](/sdk/data-coding-gap-event/#validatecodinggapevent)[] ### DetectedIssueEvidence Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean detected_issue| [DetectedIssue](/sdk/data-detected-issue/#detectedissue) ### DetectedIssueStatus Available on the model as `DetectedIssue.Status`, so a comparison reads `detected_issue.status == DetectedIssue.Status.FINAL`. Enum| Value| Label ---|---|--- REGISTERED| registered| Registered PRELIMINARY| preliminary| Preliminary CANCELLED| cancelled| Cancelled AMENDED| amended| Amended FINAL| final| Final CORRECTED| corrected| Corrected ENTERED_IN_ERROR| entered-in-error| Entered in Error ### DetectedIssueSeverity Available on the model as `DetectedIssue.Severity`. A detected issue that carries no severity holds an empty string rather than `None`. Enum| Value| Label ---|---|--- HIGH| high| High MODERATE| moderate| Moderate LOW| low| Low --- # Device Source: https://docs.canvasmedical.com/sdk/data-device/ ## Introduction The `Device` model represents a type of a manufactured item that is used in the provision of healthcare without being substantially changed through that activity. The device may be a medical or non-medical device. ## Basic usage To get a device by identifier, use the `get` method on the `Device` model manager: ```python from canvas_sdk.v1.data.device import Device device = Device.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the devices for a patient can be accessed with the `devices` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") devices = patient.devices.all() ``` ## Filtering Devices can be filtered by any attribute that exists on the model. Filtering for devices is done with the `filter` method on the `Device` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.device import Device devices = Device.objects.filter(manufacturer="ACME Biomedical", lot_number="M320") ``` ### Committed devices The `committed` method returns devices that have been committed and not entered in error: ```python from canvas_sdk.v1.data.device import Device committed_devices = Device.objects.committed() ``` ## Attributes ### Device Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note_id| Integer labeled_contains_NRL| Boolean assigning_authority| String scoping_entity| String udi| String di| String issuing_agency| String lot_number| String brand_name| String mri_safety_status| String version_model_number| String company_name| String gmdnPTName| String status| String expiration_date| Date expiration_date_original| String serial_number| String manufacturing_date_original| String manufacturing_date| Date manufacturer| String procedure_id| Integer --- # DiagnosticView Source: https://docs.canvasmedical.com/sdk/data-diagnostic-view/ ## Introduction The `DiagnosticView` model represents a saved combination of lab tests and questionnaire codes configured on your instance. A diagnostic view has no patient of its own — it is a reusable definition. When a diagnostic view is embedded in a note with the [Reference](/sdk/commands/#reference) command, Canvas renders that patient's results for the view's codes as a timeseries. Diagnostic views are configured by an administrator, so the set available to a plugin is whatever your instance has defined. ## Basic usage To get a diagnostic view by identifier, use the `get` method on the `DiagnosticView` model manager: ```python from canvas_sdk.v1.data import DiagnosticView view = DiagnosticView.objects.get(id="dca3a3c5-0a8e-4f7b-9c6a-1b9bf3a6e5e0") ``` To list every diagnostic view on the instance: ```python from canvas_sdk.v1.data import DiagnosticView views = DiagnosticView.objects.all() ``` ## Filtering Diagnostic views can be filtered by any attribute that exists on the model. ### By name Names are set by whoever configured the view, so match on the exact name you expect and handle the case where it is absent: ```python from canvas_sdk.v1.data import DiagnosticView a1c_view = DiagnosticView.objects.filter(name="Hemoglobin A1c").first() ``` ### By search tag `tags` is a single free-text string of search terms, not a list, so use a substring match: ```python from canvas_sdk.v1.data import DiagnosticView diabetes_views = DiagnosticView.objects.filter(tags__icontains="diabetes") ``` ## Embedding a view in a note Pass the view's `id` to the [Reference](/sdk/commands/#reference) command: ```python from canvas_sdk.commands import ReferenceCommand from canvas_sdk.v1.data import DiagnosticView def compute(): a1c_view = DiagnosticView.objects.filter(name="Hemoglobin A1c").first() if not a1c_view: return [] reference = ReferenceCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", diagnostic_view_id=a1c_view.id, ) return [reference.originate(commit=True)] ``` ## Attributes ### DiagnosticView Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime name| String (up to 100 characters) tags| String (up to 500 characters; free-text search terms) originator| [CanvasUser](/sdk/data-canvasuser) --- # DME Source: https://docs.canvasmedical.com/sdk/data-dme/ ## Introduction The DME data models let a plugin read the durable medical equipment (DME) that providers order with the [OrderDme](/sdk/commands/#orderdme) command: - `DmeProduct` is an item on your instance's DME formulary. Its `id` is the value `OrderDmeCommand` takes as `dme_product_id`, so a plugin can find a product by its code or name instead of hard-coding the ID. - `DmeOrder` is the record a signed Order DME command creates. It keeps the ordered item, indications, quantity, supplier, and ordering provider, along with the patient's height and weight when the order was signed. Both models are read-only. Admins manage the formulary in the Django Admin UI under **DME Products**. ## Basic usage ### Find a formulary product Filter `DmeProduct` by code, and include `active=True`, because the Order DME command only accepts active products: ```python from canvas_sdk.v1.data import DmeProduct wheelchair = DmeProduct.objects.filter(code="K0001", active=True).first() ``` A product can have no code, and two products can share one, so check that the lookup found a product before you use it. To match on the product name instead, use `name__icontains`. ### Order a formulary product Pass the product's `id` as `dme_product_id`: ```python from canvas_sdk.commands import OrderDmeCommand from canvas_sdk.v1.data import DmeProduct wheelchair = DmeProduct.objects.filter(code="K0001", active=True).first() if wheelchair: order = OrderDmeCommand( note_uuid="8f4b1e2c-9a3d-4c7e-b1f6-2d5a8c0e3b47", dme_product_id=str(wheelchair.id), diagnosis_codes=["R262"], quantity=1, ordering_provider_key="b8a7c6d5-4e3f-4a2b-9c1d-0e8f7a6b5c4d", ) ``` ### Read signed orders To get an order by identifier, use the `get` method on the `DmeOrder` model manager: ```python from canvas_sdk.v1.data import DmeOrder order = DmeOrder.objects.get(id="3c9e2f4a-6b1d-4e8a-9f7c-5d2b0a1e8c63") ``` If you have a patient object, the patient's orders are available from the `dme_orders` attribute: ```python from canvas_sdk.v1.data import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") orders = patient.dme_orders.all() ``` A `DmeOrder` is the [anchor object](/sdk/data-command/#the-anchor-object) of its `orderDme` command, so you can also reach it from the command: ```python from canvas_sdk.v1.data import Command command = Command.objects.get(id="c1b5a4d2-7e3f-4a8b-9c6d-2f1e0a9b8c7d") order = command.anchor_object ``` An order linked to an [assessment](/sdk/data-assessment/#commands-linked-to-an-assessment) in the same note keeps that assessment on its `assessment` field. Read the orders linked to an assessment from the assessment's side with `linked_dme_orders`: ```python from canvas_sdk.v1.data import Assessment assessment = Assessment.objects.get(id="9a4c2e7b-1f3d-4b6a-8e5c-0d2f7a9b1c34") linked_orders = assessment.linked_dme_orders.all() ``` ## Filtering DME products and orders can be filtered by any attribute that exists on the model. ### Products by code system `code_system` is a `DmeProductCodeSystem` value: `HCPCS` for Healthcare Common Procedure Coding System (HCPCS) codes, or `INTERNAL` for codes your organization assigns: ```python from canvas_sdk.v1.data import DmeProduct, DmeProductCodeSystem hcpcs_products = DmeProduct.objects.filter(code_system=DmeProductCodeSystem.HCPCS, active=True) ``` ### Committed orders The `committed` method returns orders that have been committed and not entered in error: ```python from canvas_sdk.v1.data import DmeOrder committed_orders = DmeOrder.objects.committed() ``` ## What a signed order records Canvas copies the item into the order when the command is signed, so later formulary changes don't change an existing order: - For a formulary product, `product` links to the `DmeProduct`, and `item_name`, `item_code`, and `item_code_system` hold the product's values at signing. - For an off-formulary item, `product` is `None`, `item_name` holds the `free_text_item` text, and `item_code` and `item_code_system` are empty strings. `indications` is a list of the diagnoses the order was placed for, each a dictionary with `code` and `display` keys. `patient_height` and `patient_weight` hold the patient's latest committed height and weight as display text with units, such as `"70 in"` and `"10 lbs, 8 oz"`. Each is an empty string when the patient had no reading at signing. ## Attributes ### DmeProduct Field Name| Type ---|--- id| UUID dbid| Integer name| String code| String code_system| DmeProductCodeSystem active| Boolean orders| DmeOrder[] ### DmeOrder Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) deleted| Boolean patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note/#note) assessment| [Assessment](/sdk/data-assessment/#assessment) product| DmeProduct item_name| String item_code| String item_code_system| String indications| JSON quantity| Integer length_of_need| String supplier| [ServiceProvider](/sdk/data-serviceprovider/) ordering_provider| [Staff](/sdk/data-staff/#staff) patient_height| String patient_weight| String ### DmeProductCodeSystem Value| Label ---|--- HCPCS| HCPCS INTERNAL| Internal --- # DocumentReference Source: https://docs.canvasmedical.com/sdk/data-document-reference/ The `DocumentReference` model represents references to documents stored in Canvas, such as uploaded PDFs, scanned files, and other clinical documents. Each document reference can link to a file stored in S3 and provides secure access via presigned URLs. ## Basic Usage ```python from canvas_sdk.v1.data import DocumentReference # Get a specific document reference doc_ref = DocumentReference.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") # Get all document references all_docs = DocumentReference.objects.all() ``` ## Filtering ### By patient ```python from canvas_sdk.v1.data import DocumentReference patient_docs = DocumentReference.objects.for_patient("b80b1cdc2e6a4aca90ccebc02e683f35") ``` ### By status ```python from canvas_sdk.v1.data import DocumentReference, DocumentReferenceStatus current_docs = DocumentReference.objects.filter(status=DocumentReferenceStatus.CURRENT) ``` ### By category or type ```python from canvas_sdk.v1.data import DocumentReference docs = DocumentReference.objects.filter(category__code="clinical-note") ``` ## Accessing Document Files The `document_url` property returns a presigned S3 URL for securely accessing the document file. If no S3 file is present, it falls back to the `document_absolute_url` field. ```python from canvas_sdk.v1.data import DocumentReference doc_ref = DocumentReference.objects.exclude(document="").first() # Returns a presigned S3 URL (valid for 1 hour) url = doc_ref.document_url ``` ## Attributes ### DocumentReference Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| document| String| document_absolute_url| String| document_content_type| String| business_identifier| String| originator| [CanvasUser](/sdk/data-canvasuser)| subject| [CanvasUser](/sdk/data-canvasuser)| type| DocumentReferenceCoding| category| DocumentReferenceCategory| status| DocumentReferenceStatus| date| Date| encounter| [Encounter](/sdk/data-encounter)| team| [Team](/sdk/data-team/#team)| related_object_document_title| String| related_object_document_comment| String| content_type| [ContentType](/sdk/data-content-type/) (the related object's type)| object_id| Integer (the related object's `dbid`)| related_object| Model (computed)| The SDK object the document is attached to, or `None` document_url| String (computed)| Presigned S3 URL or absolute URL ### DocumentReferenceCoding A coding entry representing the type of a document reference. Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean ### DocumentReferenceCategory A coding entry representing the category of a document reference. Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean ### DocumentReferenceStatus An enum representing the status of a document reference. Member| Value| Description ---|---|--- `CURRENT`| `current`| Current `SUPERSEDED`| `superseded`| Superseded `ENTERED_IN_ERROR`| `entered-in-error`| Entered in Error ## The related object Most document references point back at the record they were generated from — a lab report, a letter, a locked-note PDF, a patient statement, and so on. `content_type` and `object_id` form that generic link: `content_type` identifies the linked model by its stable `app_label` and lowercased `model` name, and `object_id` is that record's `dbid`. The `related_object` property resolves the link for you, returning the corresponding SDK data model instance: ```python from canvas_sdk.v1.data import DocumentReference doc_ref = DocumentReference.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") # The SDK object this document is attached to (a LabReport, Letter, ImagingReport, ...), or None. source = doc_ref.related_object ``` `related_object` returns `None` when the document has no related object (`content_type` or `object_id` is unset) or when the linked content type has no SDK data model equivalent. The content types it resolves today: `app_label` / `model`| SDK data model ---|--- `api` / `labreport`| [LabReport](/sdk/data-labs/#labreport) `api` / `imagingreport`| [ImagingReport](/sdk/data-imaging/#imagingreport) `api` / `letter`| [Letter](/sdk/data-letter/#letter) `api` / `notestatechangeevent`| [NoteStateChangeEvent](/sdk/data-note/#notestatechangeevent) `api` / `uncategorizedclinicaldocument`| [UncategorizedClinicalDocument](/sdk/data-uncategorized-clinical-document/#uncategorizedclinicaldocument) `api` / `referralreport`| [ReferralReport](/sdk/data-referral/#referralreport) `api` / `educationalmaterial`| [EducationalMaterial](/sdk/data-educational-material/#educationalmaterial) `api` / `patientadministrativedocument`| [PatientAdministrativeDocument](/sdk/data-patient-administrative-document/#patientadministrativedocument) `quality_and_revenue` / `invoicefull`| [Invoice](/sdk/data-invoice/#invoice) To go the other way — find every document reference for a given source type — resolve the [ContentType](/sdk/data-content-type/) at runtime from its stable `app_label` and `model` (never hardcode the per-environment `dbid`) and filter on it: ```python from canvas_sdk.v1.data import ContentType, DocumentReference content_type = ContentType.objects.filter( app_label="api", model="patientadministrativedocument" ).first() references = DocumentReference.objects.filter(content_type=content_type) ``` --- # Document Review Delegation Source: https://docs.canvasmedical.com/sdk/data-document-review-delegation/ ## Introduction The `DocumentReviewDelegation` model records a hand-off of a document review from one staff member to another staff member or team. When a reviewer delegates an uncategorized clinical document, Canvas stores who delegated it, who received it, the original owner, whether the recipient may apply the owner's signature, and any instructions. Delegations are an append-only log: a document has at most one **active** delegation at a time (`is_active`). Delegation is A↔B only — an owner delegates a document out, and the recipient may only route it back — so `on_behalf_of` always identifies the original owner and, when `signature_consent` is set, the staff member whose signature the recipient may apply while annotating the document. ## Basic usage To get a delegation by identifier, use the `get` method on the `DocumentReviewDelegation` model manager: ```python from canvas_sdk.v1.data import DocumentReviewDelegation delegation = DocumentReviewDelegation.objects.get(id="b5a0c1d2-e3f4-5678-9abc-def012345678") ``` If you have an [UncategorizedClinicalDocument](/sdk/data-uncategorized-clinical-document/), its delegations are available through the `delegations` and `active_delegation` accessors: ```python from canvas_sdk.v1.data import UncategorizedClinicalDocument document = UncategorizedClinicalDocument.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") # The full delegation history, oldest first. history = document.delegations # The current active delegation, or None when the document is with its owner. current = document.active_delegation if current and current.signature_consent: signer = current.on_behalf_of # whose signature the recipient may apply ``` ## Filtering Delegations can be filtered by any attribute that exists on the model. ### Active delegations ```python from canvas_sdk.v1.data import DocumentReviewDelegation active = DocumentReviewDelegation.objects.filter(is_active=True) ``` ### Delegations that granted signature consent ```python from canvas_sdk.v1.data import DocumentReviewDelegation with_consent = DocumentReviewDelegation.objects.filter(is_active=True, signature_consent=True) ``` ## Route-back Use the `is_route_back` property to tell whether an active delegation returned the document to its owner (as opposed to delegating it away): ```python from canvas_sdk.v1.data import UncategorizedClinicalDocument document = UncategorizedClinicalDocument.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") delegation = document.active_delegation if delegation and delegation.is_route_back: ... # the document is back with its owner ``` ## Attributes ### DocumentReviewDelegation Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime content_type| [ContentType](/sdk/data-content-type/) (the delegated document's type) object_id| Integer (the delegated document's `dbid`) delegated_by| [Staff](/sdk/data-staff/#staff) (who handed the document off) delegated_to_staff| [Staff](/sdk/data-staff/#staff) (recipient, if delegated to a person) delegated_to_team| [Team](/sdk/data-team/#team) (recipient, if delegated to a team) on_behalf_of| [Staff](/sdk/data-staff/#staff) (the original owner) signature_consent| Boolean (may the recipient apply the owner's signature) comment| String (instructions for the recipient) is_active| Boolean (the current delegation for the document) ## The delegated document `content_type` and `object_id` form a generic link to the document whose review was delegated: `content_type` identifies the linked model, and `object_id` is that record's `dbid`. Review delegation is currently supported only for [UncategorizedClinicalDocument](/sdk/data-uncategorized-clinical-document/) records, so `content_type` always resolves to that model and `object_id` is the document's `dbid`. The generic relation leaves room for additional document types in the future. The most direct way to work with a document's delegations is from the document itself, through its `delegations` and `active_delegation` accessors: ```python from canvas_sdk.v1.data import UncategorizedClinicalDocument document = UncategorizedClinicalDocument.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") current = document.active_delegation # the active DocumentReviewDelegation, or None history = document.delegations # every delegation hop recorded for the document ``` To go the other way — from a delegation to the document it points at — read `content_type` to learn which model `object_id` refers to, then resolve it. A [ContentType](/sdk/data-content-type/) is identified by its stable `app_label` and `model` (the lowercased model name), so branch on those rather than on the per-environment `dbid`. This keeps working if more document types become delegatable later: ```python from canvas_sdk.v1.data import DocumentReviewDelegation, UncategorizedClinicalDocument delegation = DocumentReviewDelegation.objects.get(id="b3e6f74c-2a1b-4c8d-9f2e-31842ae7d3b9") content_type = delegation.content_type # Today content_type is always api / uncategorizedclinicaldocument; object_id is its dbid. if content_type.app_label == "api" and content_type.model == "uncategorizedclinicaldocument": document = UncategorizedClinicalDocument.objects.get(dbid=delegation.object_id) ``` You can also use `content_type` to find every delegation for a given document type. Resolve the [ContentType](/sdk/data-content-type/) at runtime from its stable `app_label` and `model` — never hardcode the `dbid`, which differs per environment: ```python from canvas_sdk.v1.data import ContentType, DocumentReviewDelegation document_ct = ContentType.objects.filter(app_label="api", model="uncategorizedclinicaldocument").first() delegations = DocumentReviewDelegation.objects.filter(content_type=document_ct) ``` Exactly one of `delegated_to_staff` / `delegated_to_team` is set on a delegation. --- # EducationalMaterial Source: https://docs.canvasmedical.com/sdk/data-educational-material/ ## Introduction The `EducationalMaterial` model represents patient educational material recorded on a note through the Educational Material command — the selected article, its title and abstract, and the languages it is available in. Records are returned regardless of command state, so staged commands are included; use `committed()` to limit results to committed commands. ## Basic Usage `EducationalMaterial` records can be retrieved by their UUID `id`, their integer `dbid`, or through a patient. ```python from canvas_sdk.v1.data import EducationalMaterial # Get all educational material records materials = EducationalMaterial.objects.all() # Get a specific record by its UUID id material = EducationalMaterial.objects.get(id="c9a7b1e2-d4f3-4e6a-8b5c-0d1e2f3a4b5c") ``` If you have a `Patient` object, its educational material records can be accessed with the `education_material` reverse relation: ```python from canvas_sdk.v1.data import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") materials = patient.education_material.all() ``` ## Filtering ### By attribute ```python from canvas_sdk.v1.data import EducationalMaterial materials = EducationalMaterial.objects.filter(selected_language="en-us") ``` ### Committed records The `committed` method returns records that have been committed and not entered in error: ```python from canvas_sdk.v1.data import EducationalMaterial committed = EducationalMaterial.objects.committed() ``` ## Accessing the article PDF `EducationalMaterial` holds the article's metadata — its title, abstract, and languages — not the article file itself. When the command is committed, Canvas renders the article to a PDF and attaches it to a [DocumentReference](/sdk/data-document-reference/) with the LOINC type `34895-3` (Education note). To read a patient's education note PDFs, filter `DocumentReference` by that type and use its `document_url`: ```python from canvas_sdk.v1.data import DocumentReference education_notes = DocumentReference.objects.for_patient( "1eed3ea2a8d546a1b681a2a45de1d790" ).filter(type__code="34895-3") for note in education_notes: url = note.document_url ``` To resolve the PDF for one specific record, filter on the document's [related object](/sdk/data-document-reference/#the-related-object) instead. Resolve the [ContentType](/sdk/data-content-type/) at runtime from its stable `app_label` and `model` — never hardcode the per-environment `dbid` — and match `object_id` against the material's `dbid`: ```python from canvas_sdk.v1.data import ContentType, DocumentReference, EducationalMaterial material = EducationalMaterial.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") content_type = ContentType.objects.filter( app_label="api", model="educationalmaterial" ).first() document = DocumentReference.objects.filter( content_type=content_type, object_id=material.dbid ).first() url = document.document_url if document else None ``` > **Info:** `object_id` holds the related record's integer `dbid`, not its UUID `id`, so filter on `material.dbid`. ## Attributes ### EducationalMaterial Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note/#note) assessment| [Assessment](/sdk/data-assessment/#assessment) article_id| String selected_language| EducationalMaterialLanguage title| String languages| String[] abstract| String `selected_language` defaults to `en-us`. `languages` holds the locale codes the article is available in, drawn from the same set of codes as EducationalMaterialLanguage. It is stored as a plain array of strings rather than an enum, so compare against the code values (`"es-us"`) rather than expecting enum members. ## Enumeration types ### EducationalMaterialLanguage Value| Label ---|--- en-us| English es-us| Spanish en-ca| English CA fr-ca| French CA fr-fr| French FR da-dk| Danish DK ar-eg| Arabic Egypt ar-us| Arabic bn-us| Bengali bs-ba| Bosnian bs-us| Bosnian fa-ir| Farsi Iran fa-us| Farsi hr-hr| Croatian ht-us| Haitian ko-us| Korean ru-ru| Russian ru-us| Russian sr-us| Serbian so-so| Somalia so-us| Somalia tl-us| Tagalog vi-vn| Vietnamese vi-us| Vietnamese zh-cn| Chinese zh-us| Chinese --- # EligibilityResponse Source: https://docs.canvasmedical.com/sdk/data-eligibility-response/ ## Introduction The `EligibilityResponse` model represents a coverage eligibility (271) response returned by a payer for a patient's `Coverage`, along with the originating `EligibilityRequest` (270). An `EligibilityResponse` also derives a check `status` (Active, Inactive, or Failed) from the payer's response. ## Basic usage To get an eligibility response by identifier, use the `get` method on the `EligibilityResponse` model manager: ```python from canvas_sdk.v1.data.eligibility_response import EligibilityResponse response = EligibilityResponse.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` Eligibility requests and responses are linked to a `Coverage`. From a coverage object, use the `requests` and `eligibility_responses` attributes: ```python from canvas_sdk.v1.data.coverage import Coverage coverage = Coverage.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") requests = coverage.requests.all() responses = coverage.eligibility_responses.all() ``` ## Eligibility status `EligibilityResponse.status` returns an `EligibilityResponseStatus` derived from the payer's response — `FAILED` when the check errored, `INACTIVE` when the payer reports an inactive benefit section, otherwise `ACTIVE`: ```python from canvas_sdk.v1.data.coverage import Coverage from canvas_sdk.v1.data.eligibility_response import EligibilityResponseStatus coverage = Coverage.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") response = coverage.eligibility_responses.order_by("created").last() is_active = response is not None and response.status == EligibilityResponseStatus.ACTIVE ``` A coverage with no eligibility responses (an empty `coverage.eligibility_responses` queryset) has not been verified. `NOT_APPLICABLE`, like `UNKNOWN`, is a value returned by [`Coverage.eligibility_status`](/sdk/data-coverage/#eligibility-status), never by an individual `EligibilityResponse.status`. A single response only ever resolves to `FAILED`, `INACTIVE`, or `ACTIVE`. ## Attributes ### EligibilityRequest Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime coverage| [Coverage](/sdk/data-coverage) trading_partner_id| String member| JSON provider| JSON payload| String control_number| String ### EligibilityResponse Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime eligibility_request| EligibilityRequest coverage| [Coverage](/sdk/data-coverage) client_id| String correlation_id| String deductible| JSON out_of_pocket| JSON coverage_info| JSON payer| JSON provider| JSON service_type_codes| List[String] service_types| List[String] subscriber| JSON trading_partner_id| String valid_request| Boolean errors| List[String] eligid| String x12_response| String parsed_x12_response| JSON status| EligibilityResponseStatus (computed) eligibility_or_benefit_information| List (computed) `status` and `eligibility_or_benefit_information` are computed from `errors` and `parsed_x12_response` rather than stored, so neither can be used in `filter()`. To select responses by outcome, filter on the columns they derive from — a failed check is one with a non-empty `errors`: ```python from canvas_sdk.v1.data.eligibility_response import EligibilityResponse failed = EligibilityResponse.objects.exclude(errors=None).exclude(errors=[]) ``` ## Enumeration types ### EligibilityResponseStatus Name| Value ---|--- ACTIVE| Active INACTIVE| Inactive FAILED| Failed UNKNOWN| Unknown NOT_APPLICABLE| NotApplicable --- # Encounter Source: https://docs.canvasmedical.com/sdk/data-encounter/ ## Introduction The `Encounter` model represents a patient encounter connected to a Note in Canvas. ## Basic usage To get an encounter by identifier, use the `get` method on the `Encounter` model manager: ```python from canvas_sdk.v1.data import Encounter encounter = Encounter.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` To get an encounter from a note, use the `encounter` attribute on the `Note` object: ```python from canvas_sdk.v1.data import Note note = Note.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") encounter = note.encounter ``` Keep in mind that not all notes have an associated encounter, so sometimes `note.encounter` will be `None`. Similary, you can get a note from an `Encounter` object by using the `note` attribute: ```python from canvas_sdk.v1.data import Encounter encounter = Encounter.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") note = encounter.note ``` All encounters will have an associated note, which means `encounter.note` will never be `None`. ## Filtering Encounters can be filtered by any attribute that exists on the model. Filtering for encounters is done with the `filter` method on the `Encounter` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.encounter import Encounter, EncounterState encounters = Encounter.objects.filter(state=EncounterState.CONCLUDED) ``` ## Attributes ### Encounter Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime note| [Note](/sdk/data-note/) state| EncounterState medium| EncounterMedium start_time| DateTime end_time| DateTime document_references| QuerySet[[DocumentReference](/sdk/data-document-reference/#documentreference)] ## Enumeration types ### EncounterState Name| Value ---|--- STARTED| STA PLANNED| PLA CONCLUDED| CON CANCELLED| CAN ### EncounterMedium Name| Value ---|--- VOICE| voice VIDEO| video OFFICE| office HOME| home OFFSITE| offsite LAB| lab --- # Common Enumeration Type Source: https://docs.canvasmedical.com/sdk/data-enumeration-types/ ## Introduction This page shows common enumeration types that are used in multiple models. ## Enumeration types ### AddressState Value| Label ---|--- active| Active deleted| Deleted ### AddressType Value| Label ---|--- postal| Postal physical| Physical both| Both ### AddressUse Value| Label ---|--- home| Home work| Work temp| Temp old| Old ### AddressUseWithBilling Value| Label ---|--- home| Home work| Work temp| Temp old| Old billing| Billing ### ColorEnum Value| Label ---|--- red| Red orange| Orange yellow| Yellow olive| Olive green| Green teal| Teal blue| Blue violet| Violet purple| Purple pink| Pink brown| Brown grey| Grey black| Black ### ContactPointState Value| Label ---|--- active| Active deleted| Deleted ### ContactPointSystem Value| Label ---|--- phone| phone fax| fax email| email pager| pager other| other ### ContactPointUse Value| Label ---|--- home| Home work| Work temp| Temp old| Old other| Other mobile| Mobile automation| Automation ### DocumentReviewMode Value| Label ---|--- RR| Review required AR| Already reviewed offline RN| Review not required ### OrderStatus Value| Description ---|--- proposed| Proposed draft| Draft planned| Planned requested| Requested received| Received accepted| Accepted in-progress| In-progress review| Review completed| Completed cancelled| Cancelled suspended| Suspended rejected| Rejected failed| Failed EIE| Entered in Error ### Origin Value| Label ---|--- REF_CMD| Referral command CMP_IMG_ORD| Completing image orders IMG_REP_REV| Imaging report review LAB_RES_REV| Lab results review CON_REP_REV| Consult report review UNC_DOC_REP_REV| Uncategorized document report review ASN_NOT_PHN_REV| Assigned note/phone call for review POP_HLT_OUT| Population health outreach CMP_LAB_ORD| Completing lab orders CHT_PDF| Chart PDF EXP_CLM_SNO| Expired claim snoozed FLG_PST_REV| Flagged posting review BAT_PTN_STA| Batch patient statements INC_COV| Incomplete Coverage ### PersonSex Value| Label ---|--- F| female M| male O| other UNK| unknown ### ReviewPatientCommunicationMethod Value| Description ---|--- DM| delegate call, can leave message DA| delegate call, need patient to answer DL| delegate letter DC| do not communicate AM| already left message AR| already reviewed with patient ### ReviewStatus Value| Label ---|--- reviewing| reviewing reviewed| reviewed ### TaxIDType Value| Label ---|--- E| EIN text S| SSN --- # ExternalEvent Source: https://docs.canvasmedical.com/sdk/data-external-event/ ## Introduction The `ExternalEvent` and `ExternalVisit` models represent clinical events from external data sources such as ADT (Admission, Discharge, Transfer) feeds. These models enable tracking of patient encounters that occur outside of Canvas, such as hospital admissions, emergency room visits, or transfers between facilities. An `ExternalVisit` groups related events for a single patient visit, while `ExternalEvent` represents individual events within that visit (e.g., admission, discharge, transfer). ## Basic usage To get an external event by identifier, use the `get` method on the `ExternalEvent` model manager: ```python from canvas_sdk.v1.data.external_event import ExternalEvent event = ExternalEvent.objects.get(id="b4f8c3a1-2d5e-4f6a-8b9c-1a2b3c4d5e6f") ``` To get an external visit: ```python from canvas_sdk.v1.data.external_event import ExternalVisit visit = ExternalVisit.objects.get(id="a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d") ``` ### Accessing related models If you have an external event, you can access the associated visit and patient: ```python from canvas_sdk.v1.data.external_event import ExternalEvent event = ExternalEvent.objects.get(id="b4f8c3a1-2d5e-4f6a-8b9c-1a2b3c4d5e6f") # Access the parent visit visit = event.external_visit # Access the patient patient = event.patient ``` If you have an external visit, you can access all events within that visit: ```python from canvas_sdk.v1.data.external_event import ExternalVisit visit = ExternalVisit.objects.get(id="a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d") # Get all events in this visit events = visit.visit_events.all() ``` If you have a patient object, you can access their external events and visits: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") # Get all external events for this patient events = patient.patient_events.all() # Get all external visits for this patient visits = patient.patient_visits.all() ``` ## Filtering External events and visits can be filtered by any attribute that exists on the model. ### By patient ```python from canvas_sdk.v1.data.external_event import ExternalEvent, ExternalVisit # Get all events for a specific patient events = ExternalEvent.objects.filter(patient__id="1eed3ea2a8d546a1b681a2a45de1d790") # Get all visits for a specific patient visits = ExternalVisit.objects.filter(patient__id="1eed3ea2a8d546a1b681a2a45de1d790") ``` ### By event type ```python from canvas_sdk.v1.data.external_event import ExternalEvent # Get all admission events admissions = ExternalEvent.objects.filter(event_type="ADT^A01") # Get all discharge events discharges = ExternalEvent.objects.filter(event_type="ADT^A03") ``` ### By cancelled status ```python from canvas_sdk.v1.data.external_event import ExternalEvent # Get all non-cancelled events active_events = ExternalEvent.objects.filter(event_cancelation_datetime__isnull=True) # Get all cancelled events cancelled_events = ExternalEvent.objects.filter(event_cancelation_datetime__isnull=False) ``` ### By visit identifier ```python from canvas_sdk.v1.data.external_event import ExternalVisit visit = ExternalVisit.objects.get(visit_identifier="VISIT-12345") ``` ### By facility ```python from canvas_sdk.v1.data.external_event import ExternalVisit visits = ExternalVisit.objects.filter(facility_name="General Hospital") ``` ## Attributes ### ExternalEvent Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| external_visit| ExternalVisit| patient| [Patient](/sdk/data-patient/#patient)| message_control_id| String| message_datetime| DateTime| event_type| String| event_datetime| DateTime| event_cancelation_datetime| DateTime| raw_message| String| cancelled| Boolean (computed)| `True` when the event has an `event_cancelation_datetime` ### ExternalVisit Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime patient| [Patient](/sdk/data-patient/#patient) visit_identifier| String information_source| String facility_name| String visit_events| QuerySet[ExternalEvent] ## Common Event Types External events typically use HL7 ADT event types: Event Type| Description ---|--- ADT^A01| Admit/Visit Notification ADT^A02| Transfer a Patient ADT^A03| Discharge/End Visit ADT^A04| Register a Patient ADT^A08| Update Patient Information ADT^A11| Cancel Admit/Visit Notification ADT^A12| Cancel Transfer ADT^A13| Cancel Discharge/End Visit ```python from canvas_sdk.v1.data.external_event import ExternalEvent from logger import log # Get recent events for a patient and log their types patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" events = ExternalEvent.objects.filter(patient__id=patient_id).order_by("-event_datetime")[:10] for event in events: status = "CANCELLED" if event.cancelled else "ACTIVE" log.info(f"Event: {event.event_type} at {event.event_datetime} [{status}]") ``` --- # Facility Source: https://docs.canvasmedical.com/sdk/data-facility/ ## Introduction The `Facility` object represents a healthcare facility associated with patients within Canvas. Facilities can include hospitals, clinics, or other healthcare institutions where patients receive care. This object contains essential information about the facility, such as its address, contact details, and operational status. ## Basic Usage To get a facility by identifier, use the `get` method on the `Facility` model manager: ```python from canvas_sdk.v1.data.facility import Facility facility = Facility.objects.get(id="34b50dfa-1b3e-4dc2-a11d-41b3115c29f3") ``` ## Filtering Facilities can be filtered by any attribute that exists on the model. Filtering for facilities is done with the `filter` method on the `Facility` model manager. ## Attributes Specify attributes with `filter` to filter by those attributes: ```python from canvas_sdk.v1.data.facility import Facility facilities = Facility.objects.filter(name="General Hospital", city="Metropolis") ``` ### Facility Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime line1| String line2| String city| String district| String state_code| String postal_code| String name| String npi_number| String phone_number| String fax_number| String active| Boolean patient_facilities| QuerySet[[PatientFacilityAddress](/sdk/data-patient/#patientfacilityaddress)] --- # FamilyHistory Source: https://docs.canvasmedical.com/sdk/data-family-history/ ## Introduction The `FamilyHistory` model represents a patient's family medical history — the condition(s) recorded for one of the patient's relatives, captured by the `family_history` command. The relative is identified by a SNOMED code and term, and the condition(s) are stored as `FamilyHistoryCoding` records reachable through the `coding` accessor. ## Basic usage To get a family history record by identifier, use the `get` method on the `FamilyHistory` model manager: ```python from canvas_sdk.v1.data.family_history import FamilyHistory family_history = FamilyHistory.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, a patient's family history can be accessed with the `family_histories` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") family_histories = patient.family_histories.all() ``` If you have a patient ID, you can get the family history for the patient with the `for_patient` method on the `FamilyHistory` model manager: ```python from canvas_sdk.v1.data.family_history import FamilyHistory patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" family_histories = FamilyHistory.objects.for_patient(patient_id) ``` ## Codings The relative's condition coding records can be accessed with the `coding` attribute on a `FamilyHistory` object. `FamilyHistory` exposes this relation as the singular `coding`, unlike the plural `codings` on Condition, Procedure, and Immunization: ```python from canvas_sdk.v1.data.family_history import FamilyHistory from logger import log family_history = FamilyHistory.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") for coding in family_history.coding.all(): log.info(f"system: {coding.system}") log.info(f"code: {coding.code}") log.info(f"display: {coding.display}") ``` ## Filtering Family history records can be filtered by any attribute that exists on the model. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.family_history import FamilyHistory family_histories = FamilyHistory.objects.filter(relation_snomed_term="Mother") ``` ### By ValueSet See [Value Sets](/sdk/data-value-sets/) for the library of built-in value sets and how to create your own. Filtering by ValueSet works a little differently. The `find` method on the model manager is used to perform `ValueSet` filtering, matching against the relative's condition coding records — the `coding` accessor — not the `relation_snomed_code`/`relation_snomed_term` fields: ```python from canvas_sdk.v1.data.family_history import FamilyHistory from canvas_sdk.value_set.v2022.condition import Diabetes family_histories = FamilyHistory.objects.find(Diabetes) ``` ### By coding To filter on coding records directly instead of a value set, filter across the relation to match the relative's condition coding records: ```python from canvas_sdk.v1.data.family_history import FamilyHistory family_histories = FamilyHistory.objects.filter( coding__code__in=["44054006", "46635009"], ).distinct() ``` ## Attributes ### FamilyHistory Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime deleted| Boolean committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) relation_snomed_code| Integer relation_snomed_term| String narrative| String coding| FamilyHistoryCoding[] ### FamilyHistoryCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean family_history| FamilyHistory --- # Fax Source: https://docs.canvasmedical.com/sdk/data-fax/ ## Introduction Canvas creates a `Fax` record for every fax it sends or receives. `direction` tells the two apart. - **Sent faxes:** when a user faxes a note, referral, imaging order, lab order, letter, or integration task, Canvas also records an action event on that document. Plugins can read them to see whether a fax was delivered, who sent it, and which number it went to. - **Received faxes:** each one has a `Fax` record with the sender's number, the page count, and whether it was received successfully. Each document type has its own action event model: Document| Action event| Reverse accessor on the document ---|---|--- [Note](/sdk/data-note/)| NoteActionEvent| `action_events` [Referral](/sdk/data-referral/#referral)| ReferralActionEvent| `action_events` [ImagingOrder](/sdk/data-imaging/#imagingorder)| ImagingOrderActionEvent| `action_events` [LabOrder](/sdk/data-labs/#laborder)| LabOrderActionEvent| `action_events` [IntegrationTask](/sdk/data-integration-task/#integrationtask)| IntegrationTaskActionEvent| `action_events` [Letter](/sdk/data-letter/)| [LetterActionEvent](/sdk/data-letter-action-event/)| `letter_action_events` All six have the same action event fields and differ only in the document they point to. ## Delivery status Canvas submits a fax to the faxing service, and the service later reports whether it reached the recipient. `delivered_by_fax` on the action event holds that outcome: `delivered_by_fax`| Meaning ---|--- `None`| Submitted. The faxing service hasn't reported the outcome yet. `True`| Delivered to the recipient. `False`| Not delivered. `fax_result_msg` holds the reason the faxing service reported. A submission the faxing service rejects doesn't create an action event. So `received_by_fax` is `True` on every action event for a fax sent today, and `success` is `True` on every outbound `Fax`. Neither one tells you whether the fax was delivered. ## Basic usage ### Faxes of a document Use the document's `action_events` accessor: ```python from canvas_sdk.v1.data import Note note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") faxes = note.action_events.filter(event_type="FAXED").select_related("fax") ``` ### Failed faxes, with the sender and the number `originator` is the [CanvasUser](/sdk/data-canvasuser/) who sent the fax, and `fax.to_fax_number` is the number it was sent to: ```python from canvas_sdk.v1.data import ReferralActionEvent failed = ReferralActionEvent.objects.filter(delivered_by_fax=False).select_related( "referral", "originator", "fax" ) for event in failed: sender = event.originator failed_number = event.fax.to_fax_number if event.fax else None ``` ### Faxes sent by a plugin A fax sent with the [Fax Note effect](/sdk/effect-notes/#fax-note) is recorded as a `NoteActionEvent` on that note, with the Canvas Bot user as its `originator`. The [fax effects](/sdk/effect-fax/) for referrals, imaging orders, lab orders and letters record theirs the same way, on that document's action event model. Canvas Bot's staff id is `5eede137ecfe4124b8b773040e33be14` on every instance. To read the outcome, look the event up by the note, the number it was sent to, and that originator, so faxes staff sent to the same number are left out. Numbers are stored in E.164 format: ```python from canvas_sdk.v1.data import NoteActionEvent CANVAS_BOT_STAFF_ID = "5eede137ecfe4124b8b773040e33be14" latest = ( NoteActionEvent.objects.filter( note__id="89992c23-c298-4118-864a-26cb3e1ae822", fax__to_fax_number="+15555550100", originator__staff__id=CANVAS_BOT_STAFF_ID, ) .order_by("-created") .first() ) ``` Every plugin's faxes are attributed to Canvas Bot, so this can't tell your plugin's faxes from another plugin's. If more than one plugin faxes the same note to the same number, compare `created` with when your plugin returned the effect. ### Reacting when a fax fails Each faxed document type has [plugin events](/sdk/events/#faxed-documents) that target its action event. The `_CREATED` event fires when the fax is submitted, and the `_UPDATED` event fires when the faxing service reports the outcome: Document| Fax submitted| Outcome reported| Target ---|---|---|--- Note| `NOTE_ACTION_EVENT_CREATED`| `NOTE_ACTION_EVENT_UPDATED`| NoteActionEvent Referral| `REFERRAL_ACTION_EVENT_CREATED`| `REFERRAL_ACTION_EVENT_UPDATED`| ReferralActionEvent Imaging order| `IMAGING_ORDER_ACTION_EVENT_CREATED`| `IMAGING_ORDER_ACTION_EVENT_UPDATED`| ImagingOrderActionEvent Lab order| `LAB_ORDER_ACTION_EVENT_CREATED`| `LAB_ORDER_ACTION_EVENT_UPDATED`| LabOrderActionEvent Data integration task| `INTEGRATION_TASK_ACTION_EVENT_CREATED`| `INTEGRATION_TASK_ACTION_EVENT_UPDATED`| IntegrationTaskActionEvent Letter| `LETTER_ACTION_EVENT_CREATED`| `LETTER_ACTION_EVENT_UPDATED`| [LetterActionEvent](/sdk/data-letter-action-event/) `LETTER_ACTION_EVENT_CREATED` also fires when a letter is printed. Check that the action event's `event_type` is `FAXED` before you treat it as a fax. A later report that changes the outcome or its message fires `_UPDATED` again, so retry each action event at most once. This handler reacts to a failed note fax; the same pattern works for any row above with its own event and model: ```python from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler from canvas_sdk.v1.data import NoteActionEvent class NoteFaxFailed(BaseHandler): RESPONDS_TO = EventType.Name(EventType.NOTE_ACTION_EVENT_UPDATED) def compute(self): action_event = NoteActionEvent.objects.select_related("note", "fax").get( id=self.event.target.id ) if action_event.delivered_by_fax is not False: return [] failed_number = action_event.fax.to_fax_number if action_event.fax else None # Retry or alert, at most once per action event. return [] ``` ### From a fax A `Fax` reaches its action events through `noteactionevents`, `referralactionevents`, `imagingorderactionevents`, `laborderactionevents`, `letteractionevents`, and `integrationtaskactionevents`: ```python from canvas_sdk.v1.data import Fax fax = Fax.objects.get(id="d2a6c1f4-7b3e-4c1a-9f5e-0a8b7c6d5e4f") note_events = fax.noteactionevents.all() ``` ### Faxes Canvas receives Filter on `direction` to read received faxes. `success` is `False` when the faxing service reported a problem receiving the fax, such as a call that dropped partway through. Canvas still creates an integration task with the pages that arrived, so the document may be incomplete. ```python from canvas_sdk.v1.data import Fax, FaxDirection received = Fax.objects.filter(direction=FaxDirection.INBOUND).order_by("-date_utc") failed = received.filter(success=False) ``` On a received fax, `to_fax_number` is the Canvas line that received it. Depending on the faxing service that delivered the fax, Canvas stores that number with or without the leading `+`, such as `+15555550100` or `15555550100`. To find every fax one line received, match both forms: ```python from canvas_sdk.v1.data import Fax, FaxDirection line = "15555550100" received_on_line = Fax.objects.filter( direction=FaxDirection.INBOUND, to_fax_number__in=[line, f"+{line}"], ) ``` ## Attributes ### Fax Field Name| Type| Notes ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| fax_id| String| The faxing service's id for the fax to_fax_number| String| The number the fax was sent to, for example `+15555550100`. For a received fax, the Canvas number that received it, which can be stored without the `+` from_fax_number| String| The number the fax was sent from. For a received fax, the sender's number date_utc| DateTime| When the faxing service accepted a sent fax, or finished receiving a received fax fax_pages| Integer| The number of pages direction| FaxDirection| Whether Canvas sent or received the fax success| Boolean| For a sent fax, whether the faxing service accepted it. See Delivery status. For a received fax, whether it was received successfully noteactionevents| QuerySet[NoteActionEvent]| The note faxes sent as this fax referralactionevents| QuerySet[ReferralActionEvent]| The referral faxes sent as this fax imagingorderactionevents| QuerySet[ImagingOrderActionEvent]| The imaging order faxes sent as this fax laborderactionevents| QuerySet[LabOrderActionEvent]| The lab order faxes sent as this fax letteractionevents| QuerySet[[LetterActionEvent](/sdk/data-letter-action-event/)]| The letter faxes sent as this fax integrationtaskactionevents| QuerySet[IntegrationTaskActionEvent]| The integration task faxes sent as this fax fax_statuses| QuerySet[FaxStatusModel]| The status recorded when this fax was received ### FaxStatusModel Canvas records a status when it receives a fax: `Received`, or `Error` if the faxing service reported a problem receiving it. It's reachable through the fax's `fax_statuses` accessor and matches the fax's `success` value. Faxes sent from Canvas don't get one. Use `success` on the fax instead, which every received fax has. Field Name| Type| Notes ---|---|--- id| UUID| dbid| Integer| created| DateTime| When the status was recorded modified| DateTime| fax| Fax| The fax the status belongs to status| FaxStatus| ### Action event fields These fields are on all six action event models. Field Name| Type| Notes ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| event_type| EventType| send_fax_id| String| The faxing service's id for the fax received_by_fax| Boolean| Whether the faxing service accepted the fax delivered_by_fax| Boolean| The delivery outcome. See Delivery status. fax_result_msg| String| The reason the faxing service gave for a failed delivery originator| [CanvasUser](/sdk/data-canvasuser/)| The user who sent the fax fax| Fax| The fax record, including the number it was sent to ### NoteActionEvent The action event fields, plus: Field Name| Type ---|--- note| [Note](/sdk/data-note/) ### ReferralActionEvent The action event fields, plus: Field Name| Type ---|--- referral| [Referral](/sdk/data-referral/#referral) ### ImagingOrderActionEvent The action event fields, plus: Field Name| Type ---|--- imaging_order| [ImagingOrder](/sdk/data-imaging/#imagingorder) ### LabOrderActionEvent The action event fields, plus: Field Name| Type ---|--- lab_order| [LabOrder](/sdk/data-labs/#laborder) ### IntegrationTaskActionEvent The action event fields, plus: Field Name| Type ---|--- integration_task| [IntegrationTask](/sdk/data-integration-task/#integrationtask) ## Enumeration types ### FaxDirection Value| Label ---|--- O| Outbound I| Inbound ### FaxStatus Value| Label ---|--- P| Processing S| Sent R| Received E| Error ### Event Type Value| Label ---|--- PRINTED| Printed FAXED| Faxed --- # FollowUp Source: https://docs.canvasmedical.com/sdk/data-follow-up/ ## Introduction The `FollowUp` model is the anchor for the [Follow Up](/sdk/commands/#followup) command — a requested follow-up (recall) recorded on a Note for a Patient. ## Basic usage To get a follow up by identifier, use the `get` method on the `FollowUp` model manager: ```python from canvas_sdk.v1.data.follow_up import FollowUp follow_up = FollowUp.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient or note object, the follow ups can be accessed with the `follow_ups` attribute: ```python from canvas_sdk.v1.data.patient import Patient from canvas_sdk.v1.data.note import Note patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") follow_ups = patient.follow_ups.all() note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") follow_ups = note.follow_ups.all() ``` ## Committed follow ups The `committed` method returns follow ups that have been committed and not entered in error: ```python from canvas_sdk.v1.data.follow_up import FollowUp committed_follow_ups = FollowUp.objects.committed() ``` ## Attributes ### FollowUp Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) assessment| [Assessment](/sdk/data-assessment/#assessment) appointment_note| [Note](/sdk/data-note) requested_appointment_date| Date requested_appointment_date_original_input| String reason_for_visit| String reason_for_visit_coding| String note_to_patient| String internal_comment| String requested_appointment_type| EncounterMedium requested_note_type| [NoteType](/sdk/data-note) ## Enumeration types ### EncounterMedium Name| Value ---|--- VOICE| voice VIDEO| video OFFICE| office HOME| home OFFSITE| offsite LAB| lab --- # Goal Source: https://docs.canvasmedical.com/sdk/data-goal/ ## Introduction The `Goal` model represents a patient Goal in Canvas, which is always associated with a Note and a Patient. This page also documents UpdateGoal, the record of a goal's updates and closures, created by committing an [UpdateGoal command](/sdk/commands/#updategoal) or [CloseGoal command](/sdk/commands/#closegoal). ## Basic usage To get a goal by identifier, use the `get` method on the `Goal` model manager: ```python from canvas_sdk.v1.data.goal import Goal goal = Goal.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, or note object, the goals for a patient or note can be accessed with the `goals` attribute on a `Patient` or `Note` object: ```python from canvas_sdk.v1.data.patient import Patient from canvas_sdk.v1.data.note import Note patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") goals = patient.goals.all() note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") goals = note.goals.all() ``` `UpdateGoal` records can be queried the same way, and each one links back to the goal it updates with the `goal` attribute: ```python from canvas_sdk.v1.data.goal import UpdateGoal update = UpdateGoal.objects.get(id="61a1853f-168f-4ed3-80d2-44e5d144bcf3") goal = update.goal committed_updates = UpdateGoal.objects.committed() ``` ## Filtering Goals can be filtered by any attribute that exists on the model. Filtering for goals is done with the `filter` method on the `Goal` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.goal import Goal, GoalAchievementStatus goals = Goal.objects.filter(achievement_status=GoalAchievementStatus.IN_PROGRESS) ``` ### Committed goals The `committed` method returns goals that have been committed and not entered in error: ```python from canvas_sdk.v1.data.goal import Goal committed_goals = Goal.objects.committed() ``` ## Goal updates and closures Each change to a goal — via the [UpdateGoal](/sdk/commands/#updategoal) or [CloseGoal](/sdk/commands/#closegoal) command — is recorded as an `UpdateGoal`. Update actions revise the goal while leaving it active; close actions also move it to a closed `lifecycle_status` (e.g. `completed`, `cancelled`, `rejected`). A goal's updates are reachable through its `updates` accessor: ```python from canvas_sdk.v1.data.goal import Goal goal = Goal.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") # Every update or close recorded against this goal. updates = goal.updates.all() # The most recent committed update — the goal's current state — or None. latest = goal.updates.committed().order_by("dbid").last() ``` `UpdateGoal` carries the same status, priority, and progress fields as `Goal` (without `goal_statement` / `start_date`), plus a `goal` foreign key back to the goal it updates. Like `Goal`, its manager supports `committed()` to filter to committed, non-entered-in-error records. ## Attributes ### Goal Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) assessment| [Assessment](/sdk/data-assessment/#assessment) lifecycle_status| GoalLifecycleStatus achievement_status| GoalAchievementStatus priority| GoalPriority due_date| Date start_date| Date progress| String goal_statement| String updates| QuerySet[UpdateGoal] ### UpdateGoal An update or close action recorded against a Goal, reachable from a goal via `goal.updates`. Written by the [UpdateGoal](/sdk/commands/#updategoal) and [CloseGoal](/sdk/commands/#closegoal) commands. Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) assessment| [Assessment](/sdk/data-assessment/#assessment) goal| Goal lifecycle_status| GoalLifecycleStatus achievement_status| GoalAchievementStatus priority| GoalPriority due_date| Date progress| String ## Enumeration types ### GoalLifecycleStatus Name| Value ---|--- PROPOSED| proposed PLANNED| planned ACCEPTED| accepted ACTIVE| active ON_HOLD| on-hold COMPLETED| completed CANCELLED| cancelled REJECTED| rejected ### GoalAchievementStatus Name| Value ---|--- IN_PROGRESS| in-progress IMPROVING| improving WORSENING| worsening NO_CHANGE| no-change ACHIEVED| achieved SUSTAINING| sustaining NOT_ACHIEVED| not-achieved NO_PROGRESS| no-progress NOT_ATTAINABLE| not-attainable ### GoalPriority Name| Value ---|--- HIGH| high-priority MEDIUM| medium-priority LOW| low-priority --- # Google Calendar Source: https://docs.canvasmedical.com/sdk/data-google-calendar/ ## Introduction When a provider connects their Google account through native Google Calendar sync, Canvas imports the busy time from their selected Google calendars. Canvas stores that busy time in its own records rather than as [appointments](/sdk/data-appointment/) or [calendar events](/sdk/data-calendar/), so a scheduling or booking plugin that works out availability only from `Appointment` and `Event` misses it. The Google Calendar data models give plugins read-only access to that busy time and to the state of each provider's connection: - `GoogleCalendarBusyBlock` is a period of busy time imported from Google for a provider. - `GoogleCalendarConnection` is a provider's connection to their Google account. - `GoogleCalendar` is a calendar on that connection. Busy blocks carry only the time a provider is busy. They don't include the title or content of the Google event. These models have a `dbid` but no `id`. ## Busy blocks A provider's busy blocks are available through the `google_calendar_busy_blocks` attribute on a [Staff](/sdk/data-staff/) object. When busy time is released, its busy block stays in the table with `active` set to `False`, so filter on `active=True` to get current busy time: ```python from canvas_sdk.v1.data.staff import Staff provider = Staff.objects.get(id="4150cd20de8a470aa570a852859ac87e") busy_blocks = provider.google_calendar_busy_blocks.filter(active=True) ``` A busy block records its start and its length in minutes, not an end time. A Google event that spans several days arrives as one busy block for each day. Releasing a busy block also updates its `modified` time. A plugin that keeps its own copy of busy time can query for blocks modified since its last check and remove those where `active` is `False`. If released busy time returns to Google, Canvas creates a new busy block rather than reactivating the released one. ### Find a provider's busy time for a day This example lists a provider's current busy intervals that start on a given day, ordered by start time: ```python import datetime from canvas_sdk.v1.data.google_calendar import GoogleCalendarBusyBlock from logger import log day_start = datetime.datetime(2026, 10, 20, tzinfo=datetime.UTC) day_end = day_start + datetime.timedelta(days=1) busy_blocks = GoogleCalendarBusyBlock.objects.filter( provider__id="4150cd20de8a470aa570a852859ac87e", active=True, start_time__gte=day_start, start_time__lt=day_end, ).order_by("start_time") for block in busy_blocks: block_end = block.start_time + datetime.timedelta(minutes=block.duration_minutes) log.info(f"Busy from {block.start_time} to {block_end} at {block.location.full_name}") ``` ## Connections and calendars A provider's Google connection is available through the `google_calendar_connection` attribute on a `Staff` object. A provider who hasn't connected a Google account has no connection, so filter on the `staff` field to get `None` back instead of an exception: ```python from canvas_sdk.v1.data.google_calendar import GoogleCalendarConnection from canvas_sdk.v1.data.staff import Staff provider = Staff.objects.get(id="4150cd20de8a470aa570a852859ac87e") connection = GoogleCalendarConnection.objects.filter(staff=provider).first() ``` When `needs_reconnect` is `True`, the provider needs to reconnect their Google account in Canvas. The calendars on a connection are available through its `calendars` attribute. Only selected calendars feed busy blocks. The calendar with `is_app_created` set to `True` is the one Canvas writes appointments to: ```python from canvas_sdk.v1.data.google_calendar import GoogleCalendarConnection connection = GoogleCalendarConnection.objects.filter( staff__id="4150cd20de8a470aa570a852859ac87e" ).first() if connection is not None: busy_time_calendars = connection.calendars.filter(selected=True) canvas_calendar = connection.calendars.filter(is_app_created=True).first() ``` ## Attributes ### GoogleCalendarBusyBlock Field Name| Type| Description ---|---|--- dbid| Integer| Database identifier created| DateTime| When the record was created. modified| DateTime| When the record was last modified. provider| [Staff](/sdk/data-staff/#staff)| The provider who is busy location| [PracticeLocation](/sdk/data-practicelocation/)| The practice location the busy block applies to start_time| DateTime| When the busy time starts duration_minutes| Integer| How long the busy time lasts, in minutes active| Boolean| Whether the block is current busy time; `False` once the time is released ### GoogleCalendarConnection Field Name| Type| Description ---|---|--- dbid| Integer| Database identifier created| DateTime| When the record was created. modified| DateTime| When the record was last modified. staff| [Staff](/sdk/data-staff/#staff)| The provider who owns the connection google_account_email| String| The email address of the connected Google account needs_reconnect| Boolean| Whether the provider must reconnect their Google account calendars| GoogleCalendar[]| The Google calendars on this connection ### GoogleCalendar Field Name| Type| Description ---|---|--- dbid| Integer| Database identifier created| DateTime| When the record was created. modified| DateTime| When the record was last modified. connection| GoogleCalendarConnection| The connection the calendar belongs to summary| String| The calendar's name in Google selected| Boolean| Whether the calendar's events feed busy blocks is_app_created| Boolean| Whether this is the calendar Canvas writes appointments to --- # Group Source: https://docs.canvasmedical.com/sdk/data-group/ ## Introduction The `Group` model gives you the stable external identifier Canvas uses to expose a [Team](/sdk/data-team/#team) or a [PatientGroup](/sdk/data-patient-group/#patientgroup) as a single FHIR resource, so you can move between an SDK record and its FHIR representation without tracking a separate identifier yourself. A `Group` is a stable external identifier that Canvas attaches through a generic relation — the [`content_type`](/sdk/data-content-type/#contenttype) and `object_id` fields, the same pattern [OrganizationalEntity](/sdk/data-organizational-entity/#organizationalentity) uses — to either a `Team` or a `PatientGroup`, the record this reference calls the Group's content object. This identifier is exactly what the FHIR [Group endpoint](/api/group/) exposes as its `id`. A `Team` surfaces as a FHIR Group of type `practitioner`, and a `PatientGroup` surfaces as a FHIR Group of type `person`. Reach for `Group` when you hold a FHIR Group `id` — the stable external identifier — and need to resolve which SDK record it names, a `Team` or a `PatientGroup`. When you already have a `Team` in hand, its [`group_id`](/sdk/data-team/#reconciling-with-fhir) is the shorter path, because it exposes the FHIR Group id directly. `Group` exposes the linked record through typed property accessors. Create the underlying FHIR Group through the [FHIR API](/api/group/). To add or deactivate members of an existing patient group, use the [patient group effects](/sdk/effect-patient-group/). ## Basic usage To follow a `Group` to the record it points at, retrieve the `Group` by `id` and read whichever typed property is set: ```python from canvas_sdk.v1.data import Group group = Group.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") if group.team is not None: linked = group.team elif group.patient_group is not None: linked = group.patient_group else: linked = None ``` For a `Group` whose content object is a `Team`, `team` returns the linked [Team](/sdk/data-team/#team) and `patient_group` is `None`. For a `Group` whose content object is a `PatientGroup`, `patient_group` returns the linked [PatientGroup](/sdk/data-patient-group/#patientgroup) and `team` is `None`. When a `Group` is not backed by either type, both properties return `None`, so read whichever property is set rather than assuming one always resolves. ## Attributes ### Group Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| content_type| [ContentType](/sdk/data-content-type/#contenttype)| object_id| Integer| team| [Team](/sdk/data-team/#team) | `None` (computed)| The `Team` this group points at, or `None` when its content object is not a `Team`. patient_group| [PatientGroup](/sdk/data-patient-group/#patientgroup) | `None` (computed)| The `PatientGroup` this group points at, or `None` when its content object is not a `PatientGroup`. The `object_id` field holds the target record's `dbid` — its internal integer identifier — rather than its `id` UUID. Only two kinds of record get a `Group`, so `content_type.model` is always one of these two values. Read it to tell which one a `Group` points at, or read `team` or `patient_group` in the table below, which do the same check for you. `content_type.model`| Content object ---|--- `team`| [Team](/sdk/data-team/#team) `patientgroup`| [PatientGroup](/sdk/data-patient-group/#patientgroup) --- # HistoryOfPresentIllness Source: https://docs.canvasmedical.com/sdk/data-history-present-illness/ ## Introduction The `HistoryOfPresentIllness` model represents a History of Present Illness (HPI) recorded on a Note, and is always associated with a Note and a Patient. It is the data model behind the `hpi` command. ## Basic usage To get an HPI by identifier, use the `get` method on the `HistoryOfPresentIllness` model manager: ```python from canvas_sdk.v1.data.history_present_illness import HistoryOfPresentIllness hpi = HistoryOfPresentIllness.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, or note object, the histories of present illness for a patient or note can be accessed with the `histories_of_present_illness` attribute on a `Patient` or `Note` object: ```python from canvas_sdk.v1.data.patient import Patient from canvas_sdk.v1.data.note import Note patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") histories = patient.histories_of_present_illness.all() note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") histories = note.histories_of_present_illness.all() ``` ## Reading the narrative The HPI text is stored as a structured document in `narrative_json`. The `narrative` property renders it as plain text, so that is the field to read: ```python from canvas_sdk.v1.data.history_present_illness import HistoryOfPresentIllness hpi = HistoryOfPresentIllness.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") text = hpi.narrative ``` ## Filtering Histories of present illness can be filtered by any column on the model. Note that `narrative` is a Python property rather than a column, so it cannot be used in `filter()` — filter on `narrative_json` instead. ### Committed histories of present illness The `committed` method returns records that have been committed and not entered in error: ```python from canvas_sdk.v1.data.history_present_illness import HistoryOfPresentIllness committed_histories = HistoryOfPresentIllness.objects.committed() ``` ## Attributes ### HistoryOfPresentIllness Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) narrative_json| JSON narrative| String (computed) --- # Imaging Report Template Source: https://docs.canvasmedical.com/sdk/data-imaging-report-template/ ## Introduction The `ImagingReportTemplate`, `ImagingReportTemplateField`, and `ImagingReportTemplateFieldOption` models represent the templates used for imaging reports. Templates define the structure of an imaging report, including what fields need to be filled in and what options are available for each field. ## Basic Usage To retrieve an `ImagingReportTemplate` by identifier, use the `get` method on the model manager: ```python from canvas_sdk.v1.data.imaging import ImagingReportTemplate template = ImagingReportTemplate.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") ``` To access the fields defined in a template: ```python from canvas_sdk.v1.data.imaging import ImagingReportTemplate template = ImagingReportTemplate.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") fields = template.fields.all() ``` ## Filtering Templates can be filtered by any attribute on the models. ### By active status ```python from canvas_sdk.v1.data.imaging import ImagingReportTemplate active_templates = ImagingReportTemplate.objects.active() ``` ### By type ```python from canvas_sdk.v1.data.imaging import ImagingReportTemplate # Get custom (user-created) templates custom = ImagingReportTemplate.objects.custom() # Get built-in (system) templates builtin = ImagingReportTemplate.objects.builtin() ``` ### By search ```python from canvas_sdk.v1.data.imaging import ImagingReportTemplate results = ImagingReportTemplate.objects.search("chest x-ray") ``` ## Attributes ### ImagingReportTemplate Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime name| String long_name| String code| String code_system| String search_keywords| String active| Boolean custom| Boolean rank| Integer fields| ImagingReportTemplateField[] ### ImagingReportTemplateField Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime report_template| ImagingReportTemplate sequence| Integer code| String code_system| String label| String units| String type| String required| Boolean options| ImagingReportTemplateFieldOption[] ### ImagingReportTemplateFieldOption Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime field| ImagingReportTemplateField label| String key| String --- # Imaging Source: https://docs.canvasmedical.com/sdk/data-imaging/ ## Introduction The `ImagingOrder`, `ImagingReview`, `ImagingReport`, and `ImagingReportCoding` models represent imaging results. ## Basic Usage To retrieve an `ImagingOrder`, `ImagingReview`, or `ImagingReport` by identifier, use the `get` method on the model manager: ```python from canvas_sdk.v1.data.imaging import ImagingOrder, ImagingReview, ImagingReport imaging_order = ImagingOrder.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") imaging_review = ImagingReview.objects.get(id="c02c6b02-2581-46bf-819c-b5aacad2134c") imaging_report = ImagingReport.objects.get(id="c1a5a35a-4ee2-4a0e-85c0-21739dc8c4a8") ``` If you have a patient object, the orders, reviews, and reports can be accessed with the `imaging_orders`, `imaging_reviews`, and `imaging_results` attributes, respectively on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") orders = patient.imaging_orders.all() reviews = patient.imaging_reviews.all() reports = patient.imaging_results.all() ``` ## Filtering Imaging orders, reviews, and reports can be filtered by any attribute that exists on the models. Filtering is done with the `filter` method on the `ImagingOrder`, `ImagingReview`, and `ImagingReport` model managers. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.imaging import ImagingOrder, ImagingReview, ImagingReport orders = ImagingOrder.objects.filter(status="completed") reviews = ImagingReview.objects.filter(is_released_to_patient=False) reports = ImagingReport.objects.filter(requires_signature=True) ``` ### By ValueSet See [Value Sets](/sdk/data-value-sets/) for the library of built-in value sets and how to create your own. `ImagingReport` supports `ValueSet` filtering through the `find` method on its model manager: ```python from canvas_sdk.v1.data.imaging import ImagingReport from canvas_sdk.value_set.v2022.diagnostic_study import Mammography reports = ImagingReport.objects.find(Mammography) ``` `find` joins through the report's `codings` reverse relation and matches on `(system, code)` pairs from the value set, so a coding must match both the code system and the code to be included. ### Committed records The `committed` method returns `ImagingOrder` and `ImagingReview` records that have been committed and not entered in error: ```python from canvas_sdk.v1.data.imaging import ImagingOrder, ImagingReview committed_orders = ImagingOrder.objects.committed() committed_reviews = ImagingReview.objects.committed() ``` ## Related Tasks To retrieve an Imaging Order's related tasks, use the `get_task_objects` method on the ImagingOrder object. ```python from canvas_sdk.v1.data.imaging import ImagingOrder imaging_order = ImagingOrder.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") tasks = imaging_order.get_task_objects().all() ``` The `task_list` computed property returns the same related tasks as a `list[Task]`: ```python from canvas_sdk.v1.data.imaging import ImagingOrder imaging_order = ImagingOrder.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") tasks = imaging_order.task_list ``` ## Accessing the report file The `document_url` property on `ImagingReport` returns a presigned S3 URL for securely accessing the report's file. The URL is valid for one hour and is regenerated on each access, so don't persist or cache it. If the report has no associated file, `document_url` returns `None`. ```python from canvas_sdk.v1.data.imaging import ImagingReport imaging_report = ImagingReport.objects.get(id="c1a5a35a-4ee2-4a0e-85c0-21739dc8c4a8") # Presigned S3 URL to the report file, or None if the report has no file url = imaging_report.document_url ``` ## Accessing the imaging order PDF The `document_url` property on `ImagingOrder` returns a presigned S3 URL for the order's PDF, or `None` when the order has none. Canvas saves the PDF the first time the order is faxed. Printing the order doesn't save it, so an order that has never been faxed has no file. The URL is valid for one hour, so generate it when you need it rather than storing it. ```python from canvas_sdk.v1.data.imaging import ImagingOrder imaging_order = ImagingOrder.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") # Presigned S3 URL to the order PDF, or None if no PDF has been saved url = imaging_order.document_url ``` ## Review assignment An imaging report is assigned for review to staff members or to a team. `reviewers` holds the assigned staff members, and `team` holds the assigned team, or `None` when no team is assigned. ```python from canvas_sdk.v1.data import ImagingReport, Staff imaging_report = ImagingReport.objects.get(id="c1a5a35a-4ee2-4a0e-85c0-21739dc8c4a8") # The staff members and the team the imaging report is assigned to reviewers = imaging_report.reviewers.all() team = imaging_report.team # Every imaging report assigned to this staff member staff = Staff.objects.get(id="4150cd20de8a470aa570a852859ac87e") assigned = ImagingReport.objects.filter(reviewers__id=staff.id) ``` ## The document reference A report that has a file also has a [DocumentReference](/sdk/data-document-reference/#the-related-object) pointing back at it — the record that carries the report's document coding, category, and status, and that represents it in the FHIR API. `document_url` above is the direct route to the file itself; reach for the document reference when you want that surrounding metadata. Resolve the [ContentType](/sdk/data-content-type/) at runtime from its stable `app_label` and `model` — never hardcode the per-environment `dbid` — and match `object_id` against the report's `dbid`: ```python from canvas_sdk.v1.data import ContentType, DocumentReference, ImagingReport imaging_report = ImagingReport.objects.get(id="c1a5a35a-4ee2-4a0e-85c0-21739dc8c4a8") content_type = ContentType.objects.filter(app_label="api", model="imagingreport").first() document = DocumentReference.objects.filter( content_type=content_type, object_id=imaging_report.dbid ).first() ``` > **Info:** `object_id` holds the related record's integer `dbid`, not its UUID `id`. A report with no file has no document reference, so handle `None`. ## Attributes ### ImagingOrder Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note/#note) assessment| [Assessment](/sdk/data-assessment/#assessment) imaging| String imaging_center| [ServiceProvider](/sdk/data-serviceprovider/#service-provider) note_to_radiologist| String internal_comment| String status| [OrderStatus](/sdk/data-enumeration-types/#orderstatus) date_time_ordered| DateTime ordering_provider| [Staff](/sdk/data-staff/#staff) priority| String delegated| Boolean document| File (S3 key) document_url| String (computed) task_ids| String results| ImagingReport[] action_events| [ImagingOrderActionEvent](/sdk/data-fax/#imagingorderactionevent)[] ### ImagingReview Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient_communication_method| [ReviewPatientCommunicationMethod](/sdk/data-enumeration-types/#reviewpatientcommunicationmethod) internal_comment| String message_to_patient| String is_released_to_patient| Boolean status| [ReviewStatus](/sdk/data-enumeration-types/#reviewstatus) note| [Note](/sdk/data-note/#note) patient| [Patient](/sdk/data-patient/#patient) ### ImagingReport Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| review_mode| [DocumentReviewMode](/sdk/data-enumeration-types/#documentreviewmode)| junked| Boolean| requires_signature| Boolean| assigned_date| DateTime| assigned_by| [CanvasUser](/sdk/data-canvasuser)| reviewers| [Staff](/sdk/data-staff/#staff)[]| team| [Team](/sdk/data-team/#team)| team_assigned_date| DateTime| patient| [Patient](/sdk/data-patient/#patient)| order| ImagingOrder| source| ImagingReportSource| name| String| result_date| Date| original_date| Date| review| ImagingReview| document_url| String (computed)| Presigned S3 URL, or `None` if the report has no file codings| ImagingReportCoding[]| ### ImagingReportCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime report| ImagingReport system| String version| String code| String display| String user_selected| Boolean value| String ## Enumeration types ### ImagingReportSource Value| Label ---|--- RADIOLOGY_PATIENT| Radiology Report From Patient VERBAL_PATIENT| Verbal Report From Patient DIRECTLY_RADIOLOGY| Directly Radiology Report --- # Immunization Source: https://docs.canvasmedical.com/sdk/data-immunization/ ## Introduction The `Immunization` model represents a record of immunization events and immunization statements for a patient. Immunizations can be actively administered medications or historical records of immunizations received elsewhere. The `ImmunizationStatement` model represents historical immunization records and vaccination history. ## Basic usage To get an immunization by identifier, use the `get` method on the `Immunization` model manager: ```python from canvas_sdk.v1.data.immunization import Immunization immunization = Immunization.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the immunizations for a patient can be accessed with the `immunizations` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") immunizations = patient.immunizations.all() ``` If you have a patient ID, you can get the immunizations for the patient with the `for_patient` method on the `Immunization` model manager: ```python from canvas_sdk.v1.data.immunization import Immunization patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" immunizations = Immunization.objects.for_patient(patient_id) ``` ## Codings The codings for an immunization can be accessed with the `codings` attribute on an `Immunization` object: ```python from canvas_sdk.v1.data.immunization import Immunization from logger import log immunization = Immunization.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") for coding in immunization.codings.all(): log.info(f"system: {coding.system}") log.info(f"code: {coding.code}") log.info(f"display: {coding.display}") ``` ## Filtering Immunizations can be filtered by any attribute that exists on the model. Filtering for immunizations is done with the `filter` method on the `Immunization` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.immunization import Immunization immunizations = Immunization.objects.filter(status="completed") ``` ### By ValueSet See [Value Sets](/sdk/data-value-sets/) for the library of built-in value sets and how to create your own. Filtering by ValueSet works a little differently. The `find` method on the model manager is used to perform `ValueSet` filtering: ```python from canvas_sdk.v1.data.immunization import Immunization from canvas_sdk.value_set.v2022.immunization import InfluenzaVaccine immunizations = Immunization.objects.find(InfluenzaVaccine) ``` `find` also works on the `ImmunizationStatement` model manager, matching against the statement's own coding records, which it exposes through the singular `coding` accessor (unlike `Immunization.codings`): ```python from canvas_sdk.v1.data.immunization import ImmunizationStatement from canvas_sdk.value_set.v2022.immunization import InfluenzaVaccine immunization_statements = ImmunizationStatement.objects.find(InfluenzaVaccine) ``` ### Committed and active records The `committed` method returns immunizations that have been committed and not entered in error. The `active` method is an alias for `committed` and returns the same records: ```python from canvas_sdk.v1.data.immunization import Immunization committed_immunizations = Immunization.objects.committed() active_immunizations = Immunization.objects.active() ``` The same methods are available on the `ImmunizationStatement` model manager: ```python from canvas_sdk.v1.data.immunization import ImmunizationStatement committed_statements = ImmunizationStatement.objects.committed() active_statements = ImmunizationStatement.objects.active() ``` ## Immunization Statements To work with immunization statements (historical records), use the `ImmunizationStatement` model: ```python from canvas_sdk.v1.data.immunization import ImmunizationStatement patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" immunization_statements = ImmunizationStatement.objects.for_patient(patient_id) ``` ## Attributes ### Immunization Field Name| Type ---|--- id| UUID dbid| Integer patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note/#note) assessment| [Assessment](/sdk/data-assessment/#assessment) status| ImmunizationStatus lot_number| String manufacturer| String exp_date_original| String exp_date| Date sig_original| String date_ordered| Date given_by| [Staff](/sdk/data-staff/#staff) consent_given| Boolean take_quantity| Float dose_form| String route| String frequency_normalized_per_day| Float committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) originator| [CanvasUser](/sdk/data-canvasuser) created| DateTime modified| DateTime codings| ImmunizationCoding[] ### ImmunizationCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean immunization| Immunization ### ImmunizationStatement Field Name| Type ---|--- id| UUID dbid| Integer patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note/#note) date_original| String date| Date evidence| String comment| String reason_not_given| ImmunizationReasonsNotGiven committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) originator| [CanvasUser](/sdk/data-canvasuser) created| DateTime modified| DateTime coding| ImmunizationStatementCoding[] ### ImmunizationStatementCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean immunization_statement| ImmunizationStatement ## Enumeration types ### ImmunizationStatus Value| Label ---|--- in-progress| In Progress on-hold| on-hold completed| completed stopped| stopped ### ImmunizationReasonsNotGiven Value| Label ---|--- NA| not applicable IMMUNE| immunity MEDPREC| medical precaution OSTOCK| product out of stock PATOBJ| patient objection --- # Instruction Source: https://docs.canvasmedical.com/sdk/data-instruction/ ## Introduction The `Instruction` model represents an `Instruct` command in a patient's note — for example, "cessation of smoking" counseling, dietary instructions, or any other piece of clinical guidance recorded as an Instruct command. Instructions are included regardless of command state (staged or committed); use `.committed()` to filter to only committed commands. Querying `Instruction` from a plugin is the recommended way to ask "has this patient been given an instruction in this value set?" — for example, when computing quality measures that look for tobacco cessation counseling or dialysis education. ## Basic usage To get an instruction by identifier, use the `get` method on the `Instruction` model manager: ```python from canvas_sdk.v1.data.instruction import Instruction instruction = Instruction.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the instructions for a patient can be accessed with the `instructions` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") # Returns all instructions for the patient, regardless of command state (staged or committed) instructions = patient.instructions.all() ``` If you have a patient ID, you can get the instructions for the patient with the `for_patient` method on the `Instruction` model manager: ```python from canvas_sdk.v1.data.instruction import Instruction patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" # All instructions for the patient, regardless of command state (staged or committed) instructions = Instruction.objects.for_patient(patient_id) # Only committed instructions for the patient committed_instructions = Instruction.objects.for_patient(patient_id).committed() ``` ## Codings The codings for an instruction can be accessed with the `codings` attribute on an `Instruction` object: ```python from canvas_sdk.v1.data.instruction import Instruction from logger import log instruction = Instruction.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") for coding in instruction.codings.all(): log.info(f"system: {coding.system}") log.info(f"code: {coding.code}") log.info(f"display: {coding.display}") ``` Instruct commands originated through the SDK use either the SNOMED CT code system or an internal "unstructured" system for free-text instructions. See the [InstructCommand](/sdk/commands/#instruct) effect for the write-side details. ## Committed instructions The `committed` method returns instructions whose underlying command has been committed and not entered in error: ```python from canvas_sdk.v1.data.instruction import Instruction committed_instructions = Instruction.objects.committed() ``` ## Filtering Instructions can be filtered by any attribute that exists on the model. Filtering for instructions is done with the `filter` method on the `Instruction` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.instruction import Instruction instructions = Instruction.objects.filter(note__id="2c91b0d8-7b9d-4ef1-89e2-1f9a3a8a2b14") ``` ### By ValueSet See [Value Sets](/sdk/data-value-sets/) for the library of built-in value sets and how to create your own. Filtering by ValueSet works a little differently. The `find` method on the model manager is used to perform `ValueSet` filtering: ```python from canvas_sdk.v1.data.instruction import Instruction from canvas_sdk.value_set.v2022.intervention import TobaccoUseCessationCounseling cessation_counseling = ( Instruction.objects .for_patient("1eed3ea2a8d546a1b681a2a45de1d790") .committed() .find(TobaccoUseCessationCounseling) ) ``` `find` joins through the `codings` reverse relation and filters on `(system, code)` pairs from the value set, so it composes naturally with `for_patient` and `committed`. ## Attributes ### Instruction Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note/#note) assessment| [Assessment](/sdk/data-assessment/#assessment) narrative| String codings| InstructionCoding[] ### InstructionCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean instruction| Instruction --- # Integration Task Source: https://docs.canvasmedical.com/sdk/data-integration-task/ ## Introduction The `IntegrationTask` and `IntegrationTaskReview` models represent incoming documents that need processing in Canvas. Integration tasks are created when documents arrive via fax, document upload, integration engine, or the patient portal. Each task can have one or more reviews that track who is responsible for processing the document and its current state. ## Basic Usage To retrieve an `IntegrationTask` or `IntegrationTaskReview` by identifier, use the `get` method on the model manager: ```python from canvas_sdk.v1.data.integration_task import IntegrationTask, IntegrationTaskReview task = IntegrationTask.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") review = IntegrationTaskReview.objects.get(id="c1a5a35a-4ee2-4a0e-85c0-21739dc8c4a8") ``` If you have a patient object, integration tasks can be accessed with the `integration_tasks` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") tasks = patient.integration_tasks.all() ``` ## Accessing the document file The `document_url` property returns a presigned S3 URL for the document as Canvas received it, or `None` when the task has no file. The URL is valid for one hour, so generate it when you need it rather than storing it. ```python from canvas_sdk.v1.data.integration_task import IntegrationTask task = IntegrationTask.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") # Presigned S3 URL to the received document, or None url = task.document_url ``` > **Info:** `document_url` always points to the original file. Canvas saves staff annotations as separate files that the SDK doesn't expose, so the URL doesn't reflect them. ## Filtering Integration tasks and reviews can be filtered by any attribute that exists on the models. ### By status Filter tasks by their processing status: ```python from canvas_sdk.v1.data.integration_task import IntegrationTask # Get all unread tasks unread = IntegrationTask.objects.unread() # Get tasks pending review (UNREAD or READ) pending = IntegrationTask.objects.pending_review() # Get processed tasks (PROCESSED or REVIEWED) processed = IntegrationTask.objects.processed() # Get tasks with errors errored = IntegrationTask.objects.with_errors() # Get non-junked tasks active = IntegrationTask.objects.not_junked() ``` ### By channel Filter tasks by their source channel: ```python from canvas_sdk.v1.data.integration_task import IntegrationTask faxes = IntegrationTask.objects.faxes() uploads = IntegrationTask.objects.uploads() engine_tasks = IntegrationTask.objects.from_integration_engine() portal_tasks = IntegrationTask.objects.from_patient_portal() ``` ### By patient ```python from canvas_sdk.v1.data.integration_task import IntegrationTask tasks = IntegrationTask.objects.for_patient("patient-id") ``` ### Filtering reviews ```python from canvas_sdk.v1.data.integration_task import IntegrationTaskReview # Get reviews for a specific task reviews = IntegrationTaskReview.objects.for_task("task-id") # Get active (non-junked) reviews active_reviews = IntegrationTaskReview.objects.active() # Get reviews by a specific reviewer reviewer_reviews = IntegrationTaskReview.objects.by_reviewer("staff-id") # Get reviews assigned to a specific team team_reviews = IntegrationTaskReview.objects.by_team("team-id") ``` ## Attributes ### IntegrationTask Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| status| IntegrationTaskStatus| type| String| title| String| document| File (S3 key)| The document as received channel| IntegrationTaskChannel| patient| [Patient](/sdk/data-patient/#patient)| service_provider| [ServiceProvider](/sdk/data-serviceprovider/#service-provider)| reviews| IntegrationTaskReview[]| action_events| [IntegrationTaskActionEvent](/sdk/data-fax/#integrationtaskactionevent)[]| Faxes of this task, with their [delivery status](/sdk/data-fax/#delivery-status) document_url| String (computed)| Presigned S3 URL for `document`, or `None` if unset is_fax| Boolean (computed)| Whether this is a fax task is_pending| Boolean (computed)| Whether this task is pending review is_processed| Boolean (computed)| Whether this task has been processed has_error| Boolean (computed)| Whether this task has an error is_junked| Boolean (computed)| Whether this task is junked ### IntegrationTaskReview Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| task| IntegrationTask| template_name| String| document_key| String| reviewer| [Staff](/sdk/data-staff/#staff)| team_reviewer| [Team](/sdk/data-team/#team)| junked| Boolean| is_active| Boolean (computed)| Whether this review is active (not junked) ## Enumeration types ### IntegrationTaskStatus Value| Label ---|--- UNR| Unread UER| Unread Error REA| Read ERR| Error PRO| Processed REV| Reviewed JUN| Junk ### IntegrationTaskChannel Value| Label ---|--- fax| Fax document_upload| Document Upload from_integration_engine| From Integration Engine from_patient_portal| From Patient Portal --- # Invoice Source: https://docs.canvasmedical.com/sdk/data-invoice/ ## Introduction The `Invoice` model represents a statement generated for a patient or their guarantor — who it was addressed to, what it totals, how it was sent, and where it stands. Invoices are produced by Canvas billing workflows rather than by plugins: automated statement runs, batch runs, and one-off statements each record their origin in `workflow`. > **Info:** Invoice records have no UUID `id` — they are identified by their integer `dbid`. ## Basic Usage ```python from canvas_sdk.v1.data import Invoice invoice = Invoice.objects.get(dbid=42) ``` If you have a `Patient` object, the statements addressed to them are available through the `invoices` reverse relation: ```python from canvas_sdk.v1.data import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") invoices = patient.invoices.all() ``` A [Claim](/sdk/data-claim/#claim) points at the most recent statement it appeared on: ```python from canvas_sdk.v1.data import Claim claim = Claim.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") invoice = claim.latest_invoice ``` ## Filtering ```python from canvas_sdk.v1.data.invoice import Invoice, InvoiceStatus, InvoiceWorkflow # Active statements only active = Invoice.objects.filter(status=InvoiceStatus.ACTIVE) # Statements a staff member generated one at a time, rather than by a batch or automated run adhoc = Invoice.objects.filter(workflow=InvoiceWorkflow.ADHOC) ``` `Invoice` is addressed through `recipient` rather than a `patient` field, so filter on `recipient` to scope to one patient: ```python from canvas_sdk.v1.data import Invoice invoices = Invoice.objects.filter(recipient__id="1eed3ea2a8d546a1b681a2a45de1d790") ``` ## Accessing the statement PDF The `invoice_pdf_url` property returns a presigned S3 URL for the statement's PDF, or `None` when the invoice has no PDF. The URL is valid for one hour, so generate it when you need it rather than storing it. ```python from canvas_sdk.v1.data import Invoice invoice = Invoice.objects.get(dbid=42) # Presigned S3 URL to the statement PDF, or None if there is no PDF url = invoice.invoice_pdf_url ``` ## The document reference Canvas also attaches the same PDF to a [DocumentReference](/sdk/data-document-reference/#the-related-object), which carries the statement's document coding and status and represents it in the FHIR API. Use `invoice_pdf_url` to get the file itself, and the document reference when you want that metadata. To find it, resolve the [ContentType](/sdk/data-content-type/) for the invoice and match `object_id` against the invoice's `dbid`: ```python from canvas_sdk.v1.data import ContentType, DocumentReference, Invoice invoice = Invoice.objects.get(dbid=42) content_type = ContentType.objects.filter( app_label="quality_and_revenue", model="invoicefull" ).first() document = DocumentReference.objects.filter( content_type=content_type, object_id=invoice.dbid ).first() url = document.document_url if document else None ``` ## Attributes ### Invoice Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser/) recipient| [Patient](/sdk/data-patient/#patient) recipient_type| InvoiceRecipients total_amount| Decimal invoice_pdf| File (S3 key) invoice_pdf_url| String (computed) status| InvoiceStatus workflow| InvoiceWorkflow error_message| String sent_mean| InvoiceSentMeans `error_message` carries the reason a statement failed to go out, and is empty for statements that did not fail. ## Enumeration types ### InvoiceRecipients Who the statement was addressed to. Value| Label ---|--- patient| Patient guarantor| Guarantor ### InvoiceStatus Value| Label ---|--- active| Active error| Error archived| Archived ### InvoiceWorkflow How the statement was produced. Value| Label ---|--- automated| Automated adhoc| Adhoc batch| Batch ### InvoiceSentMeans Value| Label ---|--- mail| Mail e-mail| E-mail --- # LabPartner, LabPartnerTest & AOE Questions Source: https://docs.canvasmedical.com/sdk/data-lab-partner-and-test/ ## Introduction The **LabPartner** , **LabPartnerTest** , **LabPartnerTestQuestion** , and **LabPartnerTestQuestionChoice** models represent external lab partners, the tests they offer, and the ask-at-order-entry (AOE) questions associated with each test within Canvas. * * * ## LabPartner The `LabPartner` model stores information about a lab partner ### Basic Usage To retrieve a lab partner by its unique identifier: ```python from canvas_sdk.v1.data.lab import LabPartner lab_partner = LabPartner.objects.get(id="your-uuid-here") ``` You can also filter lab partners by attributes. For example, to list all active lab partners: ```python from canvas_sdk.v1.data.lab import LabPartner active_lab_partners = LabPartner.objects.filter(active=True) ``` ## LabPartnerTest The `LabPartnerTest` model represents a test offered by a lab partner. Each test is linked to a lab partner via a foreign key. ### Basic Usage To retrieve tests for a given lab partner, you can access the related tests using the reverse relationship: ```python from canvas_sdk.v1.data.lab import LabPartner lab_partner = LabPartner.objects.get(id="your-uuid-here") tests = lab_partner.available_tests.all() ``` Alternatively, you can directly filter tests by attributes: ```python from canvas_sdk.v1.data.lab import LabPartnerTest tests_with_code = LabPartnerTest.objects.filter(order_code="XYZ123") ``` ## Attributes ### LabPartner Field Name| Type| Description ---|---|--- id| UUID| The universally unique identifier for the lab partner. dbid| Integer| The internal database identifier (primary key) for the lab partner. created| DateTime| When the record was created. modified| DateTime| When the record was last modified. name| String| The name of the lab partner. active| Boolean| Indicates whether the lab partner is currently active. electronic_ordering_enabled| Boolean| Indicates if electronic ordering is enabled for this lab partner. keywords| Text| Keywords associated with the lab partner. default_lab_account_number| String| The default lab account number used for orders. available_tests| LabPartnerTest[]| The tests offered by this lab partner (reverse relation, accessible via `available_tests`). ### LabPartnerTest Attributes Field Name| Type| Description ---|---|--- id| UUID| The universally unique identifier for the test record. dbid| Integer| The internal database identifier (primary key) for the test record. created| DateTime| When the record was created. modified| DateTime| When the record was last modified. lab_partner| LabPartner| A reference to the related `LabPartner` (accessible via the related name `available_tests`). order_code| String| A code used to identify the test order. May be blank. order_name| Text| The name of the test order. keywords| Text| Keywords associated with the test. May be blank. cpt_code| String| The CPT code for the test, if available. Can be blank or null. questions| LabPartnerTestQuestion[]| AOE questions associated with this test. * * * ## LabPartnerTestQuestion The `LabPartnerTestQuestion` model represents an ask-at-order-entry (AOE) question associated with a lab partner test. AOE questions are prompts that must be answered when ordering a specific lab test (e.g., "Is the patient fasting?", "Source of specimen"). ### Basic Usage To retrieve questions for a given lab partner test: ```python from canvas_sdk.v1.data.lab import LabPartnerTest test = LabPartnerTest.objects.get(id="your-uuid-here") questions = test.questions.all() ``` To filter for required questions only: ```python required_questions = test.questions.filter(required=True) ``` To directly query questions by code: ```python from canvas_sdk.v1.data.lab import LabPartnerTestQuestion questions = LabPartnerTestQuestion.objects.filter(code="FAST") ``` * * * ## LabPartnerTestQuestionChoice The `LabPartnerTestQuestionChoice` model represents a selectable answer option for an AOE question. Not all questions have predefined choices (e.g., free-text questions may have none). ### Basic Usage To retrieve choices for a given question: ```python question = test.questions.first() choices = question.choices.all() ``` ### Example: Building AOE prompts for a lab test ```python from canvas_sdk.v1.data.lab import LabPartnerTest from logger import log test = LabPartnerTest.objects.get(id="your-uuid-here") for question in test.questions.all(): log.info(f"Question: {question.body} (required={question.required})") for choice in question.choices.all(): log.info(f" - {choice.label}: {choice.value}") ``` ## Attributes ### LabPartnerTestQuestion Attributes Field Name| Type| Description ---|---|--- dbid| Integer| The internal database identifier (primary key) for the question. lab_partner_test| LabPartnerTest| A reference to the related `LabPartnerTest` (accessible via the related name `questions`). required| Boolean| Whether this question must be answered when ordering the test. code| String| A code identifying the question (e.g., "FAST" for fasting status). body| Text| The full text of the question displayed to the user. type| String| The question type (e.g., "text", "select", "date", "numeric"). created| DateTime| When the record was created. modified| DateTime| When the record was last modified. choices| LabPartnerTestQuestionChoice[]| Selectable answer options for this question. ### LabPartnerTestQuestionChoice Attributes Field Name| Type| Description ---|---|--- dbid| Integer| The internal database identifier (primary key) for the choice. lab_partner_test_question| LabPartnerTestQuestion| A reference to the related `LabPartnerTestQuestion` (accessible via the related name `choices`). label| String| The display label for this choice (shown to the user). value| String| The value submitted when this choice is selected. created| DateTime| When the record was created. modified| DateTime| When the record was last modified. --- # Lab Report Template Source: https://docs.canvasmedical.com/sdk/data-lab-report-template/ ## Introduction The `LabReportTemplate`, `LabReportTemplateField`, and `LabReportTemplateFieldOption` models represent the templates used for point-of-care (POC) labs and custom lab reports. Templates define the structure of a lab report, including what fields need to be filled in and what options are available for each field. ## Basic Usage To retrieve a `LabReportTemplate` by identifier, use the `get` method on the model manager: ```python from canvas_sdk.v1.data.lab import LabReportTemplate template = LabReportTemplate.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") ``` To access the fields defined in a template: ```python from canvas_sdk.v1.data.lab import LabReportTemplate template = LabReportTemplate.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") fields = template.fields.all() ``` ## Filtering Templates can be filtered by any attribute on the models. ### By active status ```python from canvas_sdk.v1.data.lab import LabReportTemplate # Get all active templates active_templates = LabReportTemplate.objects.active() # Get inactive templates inactive_templates = LabReportTemplate.objects.inactive() ``` ### By type ```python from canvas_sdk.v1.data.lab import LabReportTemplate # Get custom (user-created) templates custom = LabReportTemplate.objects.custom() # Get built-in (system) templates builtin = LabReportTemplate.objects.builtin() # Get point-of-care test templates poc = LabReportTemplate.objects.point_of_care() ``` ### By search ```python from canvas_sdk.v1.data.lab import LabReportTemplate results = LabReportTemplate.objects.search("glucose") ``` ## Attributes ### LabReportTemplate Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime name| String code| String code_system| String search_keywords| String active| Boolean custom| Boolean poc| Boolean fields| LabReportTemplateField[] ### LabReportTemplateField Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime report_template| LabReportTemplate sequence| Integer code| String code_system| String label| String units| String type| FieldType required| Boolean options| LabReportTemplateFieldOption[] ### LabReportTemplateFieldOption Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime field| LabReportTemplateField label| String key| String ## Enumeration types ### FieldType Value| Label ---|--- float| Float select| Select text| Text checkbox| Checkbox radio| Radio array| Array labReport| Lab Report remoteFields| Remote Fields autocomplete| Autocomplete date| Date --- # Labs Source: https://docs.canvasmedical.com/sdk/data-labs/ ## Introduction The Canvas SDK provides comprehensive models for working with laboratory data throughout the entire lab workflow—from ordering tests to reviewing results. The primary models include: - **`LabOrder`** : Represents a lab order placed for a patient, including order details, transmission type, and associated tests - **`LabTest`** : Individual tests within a lab order, tracking status from creation through processing - **`LabReport`** : Contains the results returned from the lab, including all values and associated metadata - **`LabReportRemark`** : Report-level remarks from lab personnel, accessible via `LabReport.remarks` - **`LabValue`** : Individual test results within a lab report, including values, units, and reference ranges - **`LabReview`** : Tracks the clinical review process for lab results, including provider comments and patient communication - **`DiagnosticReport`** : The `DiagnosticReport` linked to a `LabReport`, accessible via `LabReport.diagnostic_reports` ## Basic Usage To retrieve a `LabReport` model by id, use the `objects.get` method on the model. For example: ```python from canvas_sdk.v1.data.lab import LabReport lab_report = LabReport.objects.get(id="bcd287b7-8b04-4540-a1ea-6529eb576565") ``` ## Filtering To retrieve the `LabValue` instances that are associated with the `LabReport`, you can either call the `values` on the `LabReport` instance: ```python from canvas_sdk.v1.data.lab import LabReport lab_report = LabReport.objects.get(id="bcd287b7-8b04-4540-a1ea-6529eb576565") lab_values = lab_report.values.all() ``` Or query the `LabValue` model and pass the `report` argument: ```python from canvas_sdk.v1.data.lab import LabReport, LabValue lab_report = LabReport.objects.get(id="bcd287b7-8b04-4540-a1ea-6529eb576565") lab_values = LabValue.objects.filter(lab_report=lab_report) ``` Additionally, codings for lab values can be attained by querying the `LabValueCoding` model. To retrieve the codings associated with a `LabValue`, you can call `codings` on the `LabValue` instance: ```python from logger import log from canvas_sdk.v1.data.lab import LabReport, LabValue lab_report = LabReport.objects.get(id="bcd287b7-8b04-4540-a1ea-6529eb576565") lab_values = LabValue.objects.filter(lab_report=lab_report) for value in lab_values: log.info(value.codings.all()) ``` Or query the `LabValueCoding` model directly: ```python from logger import log from canvas_sdk.v1.data.lab import LabReport, LabValue, LabValueCoding lab_report = LabReport.objects.get(id="bcd287b7-8b04-4540-a1ea-6529eb576565") lab_values = LabValue.objects.filter(lab_report=lab_report) for value in lab_values: lab_value_codings = LabValueCoding.objects.filter(value=value) log.info(lab_value_codings) ``` ### Ordered vs. result tests A `LabReport` references two kinds of `LabTest` rows, and `LabReport` exposes each as its own property: - **`ordered_tests`** : `LabTest` rows created when a `LabOrder` is placed. These represent the tests that were requested and are not associated with any `LabValue` records. - **`result_tests`** : `LabTest` rows created for the results themselves. For FHIR `DiagnosticReport` and Health Gorilla ingested reports, `LabValue` records are attached to these tests. ```python from canvas_sdk.v1.data.lab import LabReport lab_report = LabReport.objects.get(id="bcd287b7-8b04-4540-a1ea-6529eb576565") for test in lab_report.ordered_tests: print(f"Ordered: {test.ontology_test_name}") for test in lab_report.result_tests: print(f"Result: {test.ontology_test_name}") for value in test.values.all(): print(f" {value.value} {value.units}") ``` When iterating many reports at once, the `LabReport` queryset exposes `with_result_tests_and_values()` to prefetch each report's result tests (with their values) and the report's full value list in bulk: ```python from canvas_sdk.v1.data.lab import LabReport reports = ( LabReport.objects .filter(patient__id="patient-id") .with_result_tests_and_values() ) ``` To query all lab reports for a particular patient, the `patient` argument can be used: ```python from logger import log from canvas_sdk.v1.data.lab import LabReport from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="6cbc40b408294a5f9b41f57ba1b2b487") lab_report = LabReport.objects.filter(patient=patient) ``` ## Example The following plugin code will run every time a new Lab Report is created and log the patient it is for, along with the values and codings from the report's results: ```python from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from logger import log from canvas_sdk.v1.data.lab import LabReport class MyHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.LAB_REPORT_CREATED) def compute(self): lab_report = LabReport.objects.select_related("patient").get(id=self.target) if lab_report.patient: log.info(f"{lab_report.patient.first_name} {lab_report.patient.last_name}") for value in lab_report.values.all(): log.info(f"{value.value} {value.units}") for coding in value.codings.all(): log.info(coding.system) log.info(coding.name) log.info(coding.code) return [] ``` For complete field documentation on all lab models, see the Attributes section below. ### Working with Lab Orders and Tests You can also work with lab orders and their associated tests. Here's an example of querying a lab order and checking the status of its tests: ```python from canvas_sdk.v1.data.lab import LabOrder, LabTest # Get a lab order by ID lab_order = LabOrder.objects.get(id="abc123...") # Access all tests in the order for test in lab_order.tests.all(): print(f"Test: {test.ontology_test_name}") print(f"Status: {test.status}") print(f"Code: {test.ontology_test_code}") # Check if results have been received if test.report: print(f"Report available with {test.report.values.count()} values") ``` ### Navigating Between Lab Orders and Reports Lab orders and lab reports are connected through the `LabTest` model. Here's how to navigate between them: #### Getting the LabOrder from a LabReport ```python from canvas_sdk.v1.data.lab import LabReport # Get a lab report lab_report = LabReport.objects.get(id="report-id") # Direct access to all orders via the reverse many-to-many relationship for lab_order in lab_report.laborder_set.all(): print(f"Order ID: {lab_order.id}") print(f"Ordered by: {lab_order.ordering_provider.full_name if lab_order.ordering_provider else 'N/A'}") print(f"Date ordered: {lab_order.date_ordered}") # Alternatively, access the order through the tests for test in lab_report.tests.all(): lab_order = test.order print(f"Order ID: {lab_order.id}") break # Usually all tests in a report share the same order ``` #### Getting LabReports from a LabOrder ```python from canvas_sdk.v1.data.lab import LabOrder # Get a lab order lab_order = LabOrder.objects.get(id="order-id") # Direct access to all reports via the many-to-many relationship for report in lab_order.reports.all(): print(f"Report ID: {report.id}") print(f"Date performed: {report.date_performed}") print(f"Number of values: {report.values.count()}") # Alternatively, access reports through the tests if you need test-level details for test in lab_order.tests.all(): if test.report: print(f"Test: {test.ontology_test_name}") print(f"Report ID: {test.report.id}") ``` ### Working with Diagnostic Reports A `LabReport` may be linked to one or more `DiagnosticReport` records. The `DiagnosticReport` model exposes its `id`, `status`, the `subject` (Patient), and the `lab` foreign key back to the originating `LabReport`. #### Getting the DiagnosticReport(s) from a LabReport ```python from canvas_sdk.v1.data.lab import LabReport lab_report = LabReport.objects.get(id="report-id") for diagnostic_report in lab_report.diagnostic_reports.all(): print(f"DiagnosticReport ID: {diagnostic_report.id}") print(f"Status: {diagnostic_report.status}") ``` #### Following a DiagnosticReport back to its LabReport ```python from canvas_sdk.v1.data.diagnostic_report import DiagnosticReport diagnostic_report = DiagnosticReport.objects.get(id="diagnostic-report-id") # Follow the `lab` foreign key back to the originating LabReport lab_report = diagnostic_report.lab if lab_report: print(f"LabReport ID: {lab_report.id}") ``` #### Filtering DiagnosticReports by patient ```python from canvas_sdk.v1.data.diagnostic_report import DiagnosticReport diagnostic_reports = DiagnosticReport.objects.for_patient("patient-id") ``` #### Reconciling with FHIR A `DiagnosticReport`'s `id` is the same id used by the FHIR API, so you can start from a `LabReport`, grab its `DiagnosticReport`, and use the FHIR client to read the corresponding FHIR [DiagnosticReport](/api/diagnosticreport/) resource: ```python from canvas_sdk.clients.canvas_fhir import CanvasFhir from canvas_sdk.v1.data.lab import LabReport lab_report = LabReport.objects.get(id="report-id") diagnostic_report = lab_report.diagnostic_reports.first() # Declare these secrets in the CANVAS_MANIFEST.json and set the values on the # plugin configuration page. client = CanvasFhir( self.secrets["CANVAS_FHIR_CLIENT_ID"], self.secrets["CANVAS_FHIR_CLIENT_SECRET"], ) # Use the DiagnosticReport's id to read the corresponding FHIR DiagnosticReport resource. fhir_diagnostic_report = client.read("DiagnosticReport", str(diagnostic_report.id)) ``` ### Working with Lab Reviews Lab reviews track the clinical review process for lab results, including provider comments and patient communication. Here's how to work with the LabReport and LabReview relationship: #### Accessing the Review from a LabReport ```python from canvas_sdk.v1.data.lab import LabReport # Get a lab report lab_report = LabReport.objects.get(id="report-id") # Check if the report has been reviewed if lab_report.review: lab_review = lab_report.review print(f"Review status: {lab_review.status}") print(f"Internal comment: {lab_review.internal_comment}") print(f"Message to patient: {lab_review.message_to_patient}") # Access the provider who reviewed it if lab_review.originator: print(f"Reviewed by: {lab_review.originator.full_name}") else: print("Report has not been reviewed yet") ``` #### Accessing Reports from a LabReview ```python from canvas_sdk.v1.data.lab import LabReview # Get a lab review lab_review = LabReview.objects.get(id="review-id") # Access all reports in this review batch for report in lab_review.reports.all(): print(f"Report ID: {report.id}") print(f"Date performed: {report.date_performed}") print(f"Number of values: {report.values.count()}") # Check if this report requires signature if report.requires_signature: print(" ⚠️ Requires provider signature") ``` #### Finding Unreviewed Lab Reports ```python from canvas_sdk.v1.data.lab import LabReport from canvas_sdk.v1.data.patient import Patient # Get all unreviewed lab reports for a patient patient = Patient.objects.get(id="patient-id") unreviewed_reports = LabReport.objects.filter( patient=patient, review__isnull=True, deleted=False ) print(f"Found {unreviewed_reports.count()} unreviewed reports") for report in unreviewed_reports: print(f"Report from {report.date_performed} - {report.values.count()} values") ``` #### Finding Lab Reports Assigned to a Reviewer A lab report is assigned for review to staff members or to a team. `reviewers` holds the assigned staff members, and `team` holds the assigned team, or `None` when no team is assigned. ```python from canvas_sdk.v1.data import LabReport, Staff lab_report = LabReport.objects.get(id="c1a5a35a-4ee2-4a0e-85c0-21739dc8c4a8") # The staff members and the team the lab report is assigned to reviewers = lab_report.reviewers.all() team = lab_report.team # Every lab report assigned to this staff member staff = Staff.objects.get(id="4150cd20de8a470aa570a852859ac87e") assigned = LabReport.objects.filter(reviewers__id=staff.id) ``` ### Filtering Lab Results by Abnormal Values A common use case is to alert a care team when an abnormal result arrives. Listen for [`LAB_REPORT_CREATED`](/sdk/events/#labs) (and the other [Lab events](/sdk/events/#labs) or [Imaging Report events](/sdk/events/#imaging-reports)) to react to new results, then inspect each `LabValue`'s `abnormal_flag` field — per Canvas, a value is flagged abnormal when `abnormal_flag` is set (non-null and non-empty). A common use case is to identify abnormal lab values that may require clinical attention: ```python from canvas_sdk.v1.data.lab import LabReport, LabValue from canvas_sdk.v1.data.patient import Patient # Get all lab reports for a patient patient = Patient.objects.get(id="patient-id") lab_reports = LabReport.objects.filter(patient=patient) # Find all abnormal values for report in lab_reports: abnormal_values = report.values.filter(abnormal_flag__isnull=False).exclude(abnormal_flag="") if abnormal_values.exists(): print(f"Report from {report.date_performed}:") for value in abnormal_values: for coding in value.codings.all(): print(f" {coding.name}: {value.value} {value.units} (Flag: {value.abnormal_flag})") ``` ### Committed records The `committed` method returns `LabReport`, `LabReview`, `LabOrder`, and `LabOrderReason` records that have been committed and not entered in error: ```python from canvas_sdk.v1.data.lab import LabReport, LabReview, LabOrder, LabOrderReason committed_reports = LabReport.objects.committed() committed_reviews = LabReview.objects.committed() committed_orders = LabOrder.objects.committed() committed_order_reasons = LabOrderReason.objects.committed() ``` ### POC (point-of-care) Lab Test results A [POC Lab Test](/sdk/commands/#poclabtest) command records its results as a `LabReport`. The command's `Command` record, with a `schema_key` of `pocLabTest`, points at that report through `anchor_object`, so you can move from the command to its results: ```python from canvas_sdk.v1.data.command import Command command = Command.objects.get(id="c1b5a4d2-7e3f-4a8b-9c6d-2f1e0a9b8c7d") lab_report = command.anchor_object ``` To react to new POC Lab Test results, listen for the [POC Lab Test command events](/sdk/events/#poc-lab-test-command), such as `POC_LAB_TEST_COMMAND__POST_COMMIT`. ## The document reference `LabReport` carries the report's values and review state, not a file. When the report is reviewed, Canvas renders it to a PDF and stores it on a [DocumentReference](/sdk/data-document-reference/#the-related-object) pointing back at the report. To find it, resolve the [ContentType](/sdk/data-content-type/) at runtime from its stable `app_label` and `model` — never hardcode the per-environment `dbid` — and match `object_id` against the report's `dbid`: ```python from canvas_sdk.v1.data import ContentType, DocumentReference, LabReport report = LabReport.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") content_type = ContentType.objects.filter(app_label="api", model="labreport").first() document = DocumentReference.objects.filter( content_type=content_type, object_id=report.dbid ).first() url = document.document_url if document else None ``` > **Info:** `object_id` holds the related record's integer `dbid`, not its UUID `id`. A report that has not been reviewed yet has no document reference, so handle `None`. ## Attributes ### LabReport Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime review_mode| [DocumentReviewMode](/sdk/data-enumeration-types/#documentreviewmode) junked| Boolean requires_signature| Boolean assigned_date| DateTime assigned_by| [CanvasUser](/sdk/data-canvasuser) reviewers| [Staff](/sdk/data-staff/#staff)[] team| [Team](/sdk/data-team/#team) team_assigned_date| DateTime patient| [Patient](/sdk/data-patient/#patient) transmission_type| TransmissionType for_test_only| Boolean external_id| String version| Integer requisition_number| String review| LabReview original_date| DateTime date_performed| DateTime custom_document_name| String originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) values| LabValue[] tests| LabTest[] ordered_tests| LabTest[] result_tests| LabTest[] remarks| LabReportRemark[] diagnostic_reports| DiagnosticReport[] laborder_set| LabOrder[] ### LabReportRemark Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime report| LabReport comment| String ### DiagnosticReport The `DiagnosticReport` linked to a `LabReport`. The `id` is the DiagnosticReport id. Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime status| DiagnosticReportStatus subject| [Patient](/sdk/data-patient/#patient) lab| LabReport ### LabReview Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) internal_comment| String message_to_patient| String status| String note| [Note](/sdk/data-note/#note) patient| [Patient](/sdk/data-patient/#patient) patient_communication_method| String reports| LabReport[] ### LabValue Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime report| LabReport value| String units| String abnormal_flag| String reference_range| String low_threshold| String high_threshold| String comment| String observation_status| String test| LabTest codings| LabValueCoding[] ### LabValueCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime value| LabValue code| String name| String system| String ### LabOrder Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note/#note) assessment| [Assessment](/sdk/data-assessment/#assessment) ontology_lab_partner| String ordering_provider| [Staff](/sdk/data-staff/#staff) comment| String requisition_number| String is_patient_bill| Boolean date_ordered| DateTime fasting_status| Boolean specimen_collection_type| SpecimenCollectionType transmission_type| TransmissionType courtesy_copy_type| CourtesyCopyType courtesy_copy_number| String courtesy_copy_text| String parent_order| LabOrder healthgorilla_id| String manual_processing_status| ManualProcessingStatus manual_processing_comment| String labcorp_abn_url| URL reasons| LabOrderReason[] tests| LabTest[] reports| LabReport[] laborder_set| LabOrder[] action_events| [LabOrderActionEvent](/sdk/data-fax/#laborderactionevent)[] ### LabOrderReason Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) order| LabOrder mode| LabReasonMode reason_conditions| LabOrderReasonCondition[] ### LabOrderReasonCondition Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime reason| LabOrderReason condition| [Condition](/sdk/data-condition) ### LabTest Represents an individual test within a lab order. Each `LabTest` tracks the lifecycle of a specific test from order creation through processing and result receipt. Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime ontology_test_name| String ontology_test_code| String status| LabTestOrderStatus report| LabReport specimen_type| String specimen_source_code| String specimen_source_description| String specimen_source_coding_system| String order| LabOrder aoe_code| String procedure_class| String values| LabValue[] ## Enumeration types ### DiagnosticReportStatus Value| Label ---|--- `REGISTERED`| Registered `PARTIAL`| Partial `PRELIMINARY`| Preliminary `FINAL`| Final `AMENDED`| Amended `CORRECTED`| Corrected `APPENDED`| Appended `CANCELLED`| Cancelled `ENTERED_IN_ERROR`| Entered-in-error `UNKNOWN`| Unknown ### TransmissionType Value| Label ---|--- F| fax H| hl7 M| manual ### SpecimenCollectionType Value| Label ---|--- L| on location P| patient service center O| other ### CourtesyCopyType Value| Label ---|--- A| account F| fax P| patient ### ManualProcessingStatus Value| Label ---|--- NEEDS_REVIEW| Needs Review IN_PROGRESS| In Progress PROCESSED| Processed FLAGGED| Flagged ### LabReasonMode Value| Label ---|--- MO| monitor IN| investigate SF| screen for UNK| unknown ### LabTestOrderStatus Value| Label ---|--- NE| new SR| staged for requisition SE| sending SF| sending failed PR| processing PF| processing failed RE| received RV| reviewed IN| inactive --- # LetterActionEvent Source: https://docs.canvasmedical.com/sdk/data-letter-action-event/ ## Introduction The `LetterActionEvent` model represents occurrences of a letter being printed or faxed within Canvas. LetterActionEvents are associated with a [Letter](/sdk/data-letter/). The other document types that can be faxed have their own action event models with the same fields. See [Fax](/sdk/data-fax/), which also explains how to read [delivery status](/sdk/data-fax/#delivery-status). ## Basic Usage ### Retrieve a specific letter action event To get a letter action event by identifier, use the `get` method on the `LetterActionEvent` model manager: ```python from canvas_sdk.v1.data.letter import LetterActionEvent letterActionEvent = LetterActionEvent.objects.get(id="b5a0c1d2-e3f4-5678-9abc-def012345678") ``` ### Find a letter action event for a specific letter If you have a letter object, you can access its associated letter_action_events using the `letter_action_events` attribute: ```python from canvas_sdk.v1.data.letter import Letter letter = Letter.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") letter_action_events = letter.letter_action_events ``` ## Filtering LetterActionEvents can be filtered by any attribute that exists on the model. ### By attribute Filtering for letter action events is done with the `filter` method on the `LetterActionEvent` model manager: ```python from canvas_sdk.v1.data.letter import LetterActionEvent # Find all successful deliveries delivered_letters = LetterActionEvent.objects.filter(delivered_by_fax=True) # Find letter action events with a specific send_fax_id letter_action_events = LetterActionEvent.objects.filter(send_fax_id="a1b2c3d4e5f6") ``` ## Attributes ### LetterActionEvent Field Name| Type| Notes ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| event_type| EventType| The type of the event send_fax_id| String| The id of the sent fax received_by_fax| Boolean| Whether the faxing service accepted the fax delivered_by_fax| Boolean| The [delivery outcome](/sdk/data-fax/#delivery-status) fax_result_msg| str| The fax result message letter| [Letter](/sdk/data-letter/)| The letter this action event is associated with originator| [User](/sdk/data-canvasuser/)| The user who printed or faxed the letter (nullable) fax| [Fax](/sdk/data-fax/#fax)| The fax record, including the number it was sent to (nullable) ## Enumeration types ### Event Type Value| Label ---|--- PRINTED| Printed FAXED| Faxed --- # Letter Source: https://docs.canvasmedical.com/sdk/data-letter/ ## Introduction The `Letter` model represents patient correspondence letters created within Canvas. Letters are associated with a [Note](/sdk/data-note/) and contain rendered content that can be printed or sent to patients. ## Basic Usage ### Retrieve a specific letter To get a letter by identifier, use the `get` method on the `Letter` model manager: ```python from canvas_sdk.v1.data.letter import Letter letter = Letter.objects.get(id="b5a0c1d2-e3f4-5678-9abc-def012345678") ``` ### Find a letter for a specific note If you have a note object, you can access its associated letter using the `letter` attribute: ```python from canvas_sdk.v1.data.note import Note note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") letter = note.letter ``` ### Find all letters created by a staff member If you have a staff object, you can find all letters they created using the `letters` attribute: ```python from canvas_sdk.v1.data.staff import Staff staff = Staff.objects.get(id="a1b2c3d4e5f6") staff_letters = staff.letters.all() ``` ## Filtering Letters can be filtered by any attribute that exists on the model. ### By attribute Filtering for letters is done with the `filter` method on the `Letter` model manager: ```python from canvas_sdk.v1.data.letter import Letter # Find all printed letters printed_letters = Letter.objects.filter(printed__isnull=False) # Find letters created by a specific staff member staff_letters = Letter.objects.filter(staff_id="a1b2c3d4e5f6") ``` ## The printed attachments `printed_document` holds the letter's attachments, merged into one PDF, not the letter body. Canvas saves it the first time the letter is printed or faxed, and reuses it for later prints and faxes. The `printed_document_url` property returns a presigned S3 URL for that PDF, or `None` when the letter hasn't been printed or faxed. The URL is valid for one hour, so generate it when you need it rather than storing it. ```python from canvas_sdk.v1.data.letter import Letter letter = Letter.objects.get(id="b5a0c1d2-e3f4-5678-9abc-def012345678") # Presigned S3 URL to the merged attachments PDF, or None attachments_url = letter.printed_document_url ``` To read the full letter as it went out, use its document reference. ## The document reference `content` holds the letter's body, not the document that goes out. Canvas renders the letter — including anything attached to it — to a PDF and stores it on a [DocumentReference](/sdk/data-document-reference/#the-related-object) pointing back at the letter. To read that PDF, resolve the [ContentType](/sdk/data-content-type/) at runtime from its stable `app_label` and `model` — never hardcode the per-environment `dbid` — and match `object_id` against the letter's `dbid`: ```python from canvas_sdk.v1.data import ContentType, DocumentReference, Letter letter = Letter.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") content_type = ContentType.objects.filter(app_label="api", model="letter").first() document = DocumentReference.objects.filter( content_type=content_type, object_id=letter.dbid ).first() url = document.document_url if document else None ``` > **Info:** `object_id` holds the related record's integer `dbid`, not its UUID `id`. The PDF is rendered after the letter is created rather than with it, so handle `None`. ## Attributes ### Letter Field Name| Type| Notes ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| content| String| The rendered letter content printed| DateTime| When the letter was printed (null if not printed) printed_document| File (S3 key)| The letter's attachments merged into one PDF, saved when the letter is first printed or faxed (null until then) printed_document_url| String (computed)| Presigned S3 URL for `printed_document`, or `None` if unset note| [Note](/sdk/data-note/)| The note this letter is associated with staff| [Staff](/sdk/data-staff/#staff)| The staff member who created the letter (nullable) letter_action_events| QuerySet[LetterActionEvent]| Action events (e.g. printed, faxed) recorded for this letter --- # Medication History Source: https://docs.canvasmedical.com/sdk/data-medication-history/ ## Introduction The `MedicationHistoryMedication` model represents historical medication data for a patient, typically imported from external sources such as health information exchanges or pharmacy systems. The `MedicationHistoryResponse` model tracks responses to medication history requests. ## Basic usage To get a medication history record by identifier, use the `get` method on the `MedicationHistoryMedication` model manager: ```python from canvas_sdk.v1.data.medication_history import MedicationHistoryMedication medication_history = MedicationHistoryMedication.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the medication history for a patient can be accessed with the `medication_history_medications` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") medication_history = patient.medication_history_medications.all() ``` ## Codings The codings for a medication history record can be accessed with the `codings` attribute on a `MedicationHistoryMedication` object: ```python from canvas_sdk.v1.data.medication_history import MedicationHistoryMedication from logger import log medication_history = MedicationHistoryMedication.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") for coding in medication_history.codings.all(): log.info(f"system: {coding.system}") log.info(f"code: {coding.code}") log.info(f"display: {coding.display}") ``` ## Filtering Medication history records can be filtered by any attribute that exists on the model. Filtering is done with the `filter` method on the `MedicationHistoryMedication` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.medication_history import MedicationHistoryMedication medications = MedicationHistoryMedication.objects.filter(dea_schedule="CII") ``` ### By date range Filter by last fill date or written date: ```python from canvas_sdk.v1.data.medication_history import MedicationHistoryMedication from datetime import datetime medications = MedicationHistoryMedication.objects.filter( last_fill_date__gte=datetime(2023, 1, 1) ) ``` ## Attributes ### MedicationHistoryMedication Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime patient| [Patient](/sdk/data-patient/#patient) drug_description| String strength_value| String strength_form| String strength_unit_of_measure| String quantity| Float quantity_unit_of_measure| String quantity_code_list_qualifier| String days_supply| Integer last_fill_date| DateTime written_date| DateTime other_date| DateTime other_date_qualifier| String substitutions| Boolean refills_remaining| Integer diagnosis_code| String diagnosis_qualifier| String diagnosis_description| String secondary_diagnosis_code| String secondary_diagnosis_qualifier| String secondary_diagnosis_description| String dea_schedule| String potency_unit_code| String etc_path_id| Array[Integer] etc_path_name| Array[String] fill_number| Integer prescriber_order_number| String source_description| String source_qualifier| String source_payer_id| String source_type| String note| String sig| String prior_authorization_status| String prior_authorization| String pharmacy_name| String pharmacy_ncpdp_id| String pharmacy_npi| String prescriber_business_name| String prescriber_first_name| String prescriber_last_name| String prescriber_npi| String prescriber_dea_number| String codings| MedicationHistoryMedicationCoding[] ### MedicationHistoryMedicationCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean medication| MedicationHistoryMedication ### MedicationHistoryResponse Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime patient| [Patient](/sdk/data-patient/#patient) staff| [Staff](/sdk/data-staff/#staff) message_id| String related_to_message_id| String status| MedicationHistoryResponseStatus reason| String reason_code| String note| String start_date| Date end_date| Date ## Enumeration types ### MedicationHistoryResponseStatus Value| Label ---|--- approved| approved denied| denied --- # Medication Statement Source: https://docs.canvasmedical.com/sdk/data-medication-statement/ ## Introduction The `MedicationStatement` model represents a record of a medication statement by a patient from the past. ## Basic usage To get a medication statement by identifier, use the `get` method on the `MedicationStatement` model manager: ```python from canvas_sdk.v1.data import MedicationStatement medication_statement = MedicationStatement.objects.get(id="61a1853f-168f-4ed3-80d2-44e5d144bcf3") ``` If you have a patient object, the medication statements for a patient can be accessed with the `medication_statements` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") medication_statements = patient.medication_statements.all() ``` You can also access the referenced medication with the `medication` attribute: ```python from canvas_sdk.v1.data import MedicationStatement medication_statement = MedicationStatement.objects.get(id="61a1853f-168f-4ed3-80d2-44e5d144bcf3") medication = medication_statement.medication ``` Or for a given medication, you can access all medication statements: ```python from canvas_sdk.v1.data import Medication medication = Medication.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") medication_statements = medication.medication_statements.all() ``` ## Latest sig A medication statement's sig is one of the sources a medication's [`latest_sig`](/sdk/data-medication/#latest-sig) draws on, alongside its active prescriptions and change medications. To get the most recent sig for the medication, read `latest_sig` from the referenced medication: ```python from canvas_sdk.v1.data import MedicationStatement medication_statement = MedicationStatement.objects.get(id="61a1853f-168f-4ed3-80d2-44e5d144bcf3") sig = medication_statement.medication.latest_sig ``` ## Committed records The `committed` method returns medication statements that have been committed and not entered in error: ```python from canvas_sdk.v1.data import MedicationStatement committed_medication_statements = MedicationStatement.objects.committed() ``` ## Attributes ### MedicationStatement Field Name| Type ---|--- id| UUID dbid| Integer patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) medication| [Medication](/sdk/data-medication) indications| [Assessment](/sdk/data-assessment)[] entered_in_error| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) originator| [CanvasUser](/sdk/data-canvasuser) created| DateTime modified| DateTime start_date_original_input| String start_date| Date end_date_original_input| String end_date| Date dose_quantity| Number dose_form| String dose_route| String dose_frequency| Number dose_frequency_interval| String sig_original_input| String --- # Medication Source: https://docs.canvasmedical.com/sdk/data-medication/ ## Introduction The `Medication` model represents a record of a medication that is being consumed by a patient, either now, in the past, or in the future. `Medication` records can represent both prescriptions and medication statements for a patient. ## Basic usage To get a medication by identifier, use the `get` method on the `Medication` model manager: ```python from canvas_sdk.v1.data.medication import Medication medication = Medication.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the medications for a patient can be accessed with the `medications` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") medications = patient.medications.all() ``` If you have a patient ID, you can get the medications for the patient with the `for_patient` method on the `Medication` model manager: ```python from canvas_sdk.v1.data.medication import Medication patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" medication = Medication.objects.for_patient(patient_id) ``` # Codings The codings for a medication can be accessed with the `codings` attribute on an `Medication` object: ```python from canvas_sdk.v1.data.medication import Medication from logger import log medication = Medication.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") for coding in medication.codings.all(): log.info(f"system: {coding.system}") log.info(f"code: {coding.code}") log.info(f"display: {coding.display}") ``` ## Filtering Medications can be filtered by any attribute that exists on the model. Filtering for medications is done with the `filter` method on the `Medication` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.medication import Medication medications = Medication.objects.filter(status="active") ``` ### By ValueSet See [Value Sets](/sdk/data-value-sets/) for the library of built-in value sets and how to create your own. Filtering by ValueSet works a little differently. The `find` method on the model manager is used to perform `ValueSet` filtering: ```python from canvas_sdk.v1.data.medication import Medication from canvas_sdk.value_set.v2022.medication import AdhdMedications medications = Medication.objects.find(AdhdMedications) ``` ## Latest sig The `latest_sig` property returns a medication's most recent sig — its directions for use — as a string. It draws on three sources: the medication's active [prescriptions](/sdk/data-prescription/), its [change medications](/sdk/data-change-medication/) that are not entered in error, and its [medication statements](/sdk/data-medication-statement/). It returns the sig from whichever of those is most recent. Because `latest_sig` is a property computed in Python, it is read from each `Medication` object and is not available to a queryset `.filter()` or `.order_by()`. When both an active prescription and a change medication exist, the source whose [note](/sdk/data-note/) has the later date of service wins. Otherwise the sig comes from the first source that has one, in this order: 1. The latest prescription. 2. The latest change medication. 3. The latest medication statement. The latest source is the one with the highest `dbid`. A prescription contributes its `combined_sig` (its sig text, plus a maximum daily dose when one is set); a change medication or medication statement contributes its sig text. When no source has a sig, `latest_sig` returns an empty string. ```python from canvas_sdk.v1.data.medication import Medication medication = Medication.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") sig = medication.latest_sig # "" when no source has a sig ``` ### Sig across many medications Reading `latest_sig` on each medication in a queryset issues a separate query per medication for the prescriptions, change medications, and medication statements `latest_sig` inspects. The `with_latest_sig` method prefetches those sources, so `latest_sig` resolves without a per-medication query: ```python from canvas_sdk.v1.data.medication import Medication patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" medications = Medication.objects.for_patient(patient_id).with_latest_sig() for medication in medications: sig = medication.latest_sig ``` ## Filtering to match the Patient Summary The Medications list in the Patient Summary section of the chart is not reproduced by filtering on `status == "active"` alone. Per Canvas, that list applies two exclusion layers before considering status: 1. Exclude uncommitted and entered-in-error records — a medication must have a non-null `committer` (it was finalized) and a null `entered_in_error` (no one flagged it as erroneous). 2. Filter by `status` — the list defaults to `status == "active"`; users can toggle to inactive or all. The `Medication` queryset has both layers built in. `committed()` keeps records with a non-null `committer` and a null `entered_in_error`, and `active()` is `committed()` plus `status == "active"`. To replicate the Patient Summary's active medications: ```python from canvas_sdk.v1.data.medication import Medication active_meds = Medication.objects.for_patient(patient_id).active() ``` For every medication the list can show (its "All" toggle, active plus inactive), use `committed()` without the status filter: ```python from canvas_sdk.v1.data.medication import Medication all_visible_meds = Medication.objects.for_patient(patient_id).committed() ``` ### How status is computed Canvas sets a medication's `status` from its command history; users do not set it directly. A medication is `inactive` when any of the following is true, and `active` otherwise: - It has a committed stop medication event that has not been entered in error. - Its prescription has a committed cancellation that has not been entered in error. - A newer medication in the same chain of commands has replaced it, for example through an adjusted prescription. - Its only commands are refill denials. - It has no committed commands linked to it, apart from ones entered in error. Canvas recomputes `status`, `start_date`, and `end_date` whenever a related command is created, committed, or entered in error. Related commands include medication statements, prescriptions, refills, adjustments, stop medication events, prescription cancellations, and eRx refill responses, and a change to a note's date of service triggers it too. The FHIR API is the exception. Creating a `MedicationStatement` through FHIR writes `status` and the `effectivePeriod` start and end straight onto the `Medication` record without recomputing them, and those values stay until a later command triggers a recomputation. ## Attributes ### Medication Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| patient| [Patient](/sdk/data-patient/#patient)| entered_in_error| [CanvasUser](/sdk/data-canvasuser)| committer| [CanvasUser](/sdk/data-canvasuser)| status| String| start_date| Date| end_date| Date| quantity_qualifier_description| String| clinical_quantity_description| String| potency_unit_code| String| national_drug_code| String| erx_quantity| String| codings| MedicationCoding[]| medication_statements| [MedicationStatement](/sdk/data-medication-statement)[]| change_medications| [ChangeMedication](/sdk/data-change-medication)[]| stopmedicationevent_set| [StopMedicationEvent](/sdk/data-stop-medication-event)[]| prescriptions| [Prescription](/sdk/data-prescription)[]| previous_medications| [Prescription](/sdk/data-prescription)[]| prescription_change_responses| [PrescriptionChangeResponse](/sdk/data-prescription-change-response/#prescriptionchangeresponse)[]| latest_sig| String (computed)| See Latest sig ### MedicationCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean medication| Medication --- # Message Source: https://docs.canvasmedical.com/sdk/data-message/ # Message Models The Canvas SDK defines messaging-related data models for sending, receiving, and tracking messages. ## TransmissionChannel A `TextChoices` enum representing the available channels for transmitting messages. Member| Value| Description ---|---|--- `MANUAL`| `manual`| Manual `TEXT_MESSAGE`| `sms`| Text Message `EMAIL`| `email`| Email `NOOP`| `noop`| No-op ## Message Represents an individual message record. ### Fields Name| Type| Description ---|---|--- `id`| `UUID`| Unique identifier for the message. `dbid`| `Integer`| Database primary key. `created`| `DateTime`| Timestamp when the message was created. `modified`| `DateTime`| Timestamp when the message was last modified. `content`| `Text`| The body text of the message. `sender`| [CanvasUser](/sdk/data-canvasuser)| The user who sent the message. May be null. `recipient`| [CanvasUser](/sdk/data-canvasuser)| The user who received the message. May be null. `note`| [Note](/sdk/data-note)| Associated note (if any) for contextual linkage. May be null. `read`| `DateTime`| Timestamp when the recipient read the message. Null if unread. `team`| [Team](/sdk/data-team/#team)| The team the message is assigned to. Null if unassigned. See [Assigning a message to a team](/sdk/effect-messages/#assigning-a-message-to-a-team). `transmissions`| QuerySet[MessageTransmission]| The delivery transmissions associated with this message. `message`| QuerySet[MessageAttachment]| The file attachments associated with this message. ## MessageAttachment Represents a file attachment linked to a message. ### Fields Name| Type| Description ---|---|--- `id`| `UUID`| Unique identifier for the attachment. `dbid`| `Integer`| Database primary key. `created`| `DateTime`| When the record was created. `modified`| `DateTime`| When the record was last modified. `file`| `Text`| Storage path or identifier for the file. `content_type`| `String`| MIME type of the attachment. `message`| Message| The parent message to which this belongs. `file_url`| String (computed)| Presigned S3 URL for accessing the file. ## MessageTransmission Tracks delivery attempts and status for a message. ### Fields Name| Type| Description ---|---|--- `id`| `UUID`| Unique identifier for the transmission record. `dbid`| `Integer`| Database primary key. `created`| `DateTime`| Timestamp when the transmission was created. `modified`| `DateTime`| Timestamp when the transmission was last modified. `message`| Message| The message associated with this transmission. `delivered`| `DateTime`| When delivery succeeded. Null if not delivered. `failed`| `DateTime`| When delivery failed. Null if it has not failed. `contact_point_system`| TransmissionChannel| The channel used for delivery. `contact_point_value`| `String`| The destination address or identifier (e.g., phone, email). `comment`| `Text`| Optional comments or error details. `delivered_by`| [Staff](/sdk/data-staff/#staff)| The staff member who processed the delivery. May be null. --- # Note Source: https://docs.canvasmedical.com/sdk/data-note/ ## Introduction The `Note` model represents clinical notes that appear on a patient's chart. A `Note` can contain multiple [commands](/sdk/data-command). ## Basic usage ### Retrieve a specific note To get a note by identifier, use the `get` method on the `Note` model manager: ```python 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: ```python 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](/sdk/data-command) for that note can be found using the `commands` attribute on the `Note` instance: ```python 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: ```python 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: ```python 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](/sdk/data-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: ```python 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: ```python 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: 1. **Text objects** : Represent free-form text content ```json {"type": "text", "value": "Patient reports feeling better"} ``` 2. **Command objects** : Reference commands with their metadata ```json { "type": "command", "value": "reasonForVisit", "data": { "command_uuid": "691123c4-6c7d-415b-880b-2beefab9f64a" } } ``` `command_uuid` identifies the command and is present on every command object. It matches the `id` of the [Command](/sdk/data-command/) model. A command object on a note that has not yet moved to the [refactored body structure](/release-notes/note-v2-2026-09-15/) can also carry an `id`, holding the integer identifier of the record the command created. A note on the refactored structure never carries one, so read the [Command](/sdk/data-command/) through `command_uuid` and take `anchor_object` from 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 > **Warning:** Naming `body` through a relation stopped working in the [September 8, 2026 release](/release-notes/1-348-0/). A queryset on another model that defers or filters `note__body` now raises an error. If you were deferring it to keep a large body out of a joined scan, query the notes you need separately with `Note.objects.defer("body")`. 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: ```python 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](/sdk/data-command/) records in a single query, the same way for every note: ```python 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")`. It never includes [Private Notes commands](/sdk/data-command/#introduction), which only their author can see. 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: ```python 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`](/sdk/data-command/) data record and read its `data` JSON — either to act on it inside the listener, or to expose it through a [`SimpleAPIRoute`](/sdk/handlers-simple-api-http/#simpleapiroute) endpoint your application can query later by command ID: ```python 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/" 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://.canvasmedical.com/plugin-io/api//routes/commands/`. ### Retrieve the audit history for a note The audit history for a note can be found using the [`NoteStateChangeEvent`](/sdk/data-note/#notestatechangeevent) model. You can access this model directly or through the `state_history` relation on the note object. ```python 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`](/sdk/data-note/#currentnotestateevent) model to check if the current note state is 'Locked'. (See: [NoteState](/sdk/data-note/#notestates) for an explanation of the different note states you might encounter) ```python 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](/sdk/data-document-reference/#the-related-object) pointing back at the [NoteStateChangeEvent](/sdk/data-note/#notestatechangeevent) that recorded the lock or signature, so you reach it through the note's state history rather than from the note itself. > **Info:** Only notes whose note type has a [category](/sdk/data-note/#notetypecategories) of `ENCOUNTER`, `INPATIENT`, or `REVIEW` are captured this way; notes in any other category have no PDF. A note can be finalized more than once. Each time captures its own PDF and Canvas supersedes the earlier ones, so filter on `CURRENT` for the version that is in force, or drop the status filter to see every captured version. #### Look up a note's PDF Resolve the [ContentType](/sdk/data-content-type/) 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: ```python 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`](/sdk/events/#document-references) 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: ```python 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](/sdk/handlers-simple-api-http/) 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`](/sdk/data-note/#currentnotestateevent) and compare against `NoteStates.SIGNED`: ```python 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`](/sdk/data-note/#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`](/sdk/data-canvasuser) who signed) and `created` timestamp: ```python 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 ``` > **Info:** The PDF reached here through `DocumentReference` (`document_url`) is one of the consumer-read copies described under [NoteStateChangeEvent](/sdk/data-note/#notestatechangeevent). On an instance with the exclusion setting enabled, the PDF omits entered-in-error commands — see that section. ### 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) ```python 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`](/sdk/data-note/#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. ```python 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. ```python 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: ```python 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: ```python 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. ```python 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](/sdk/data-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](/sdk/data-canvasuser)| provider| [Staff](/sdk/data-staff/#staff)| supervising_provider| [Staff](/sdk/data-staff/#staff)| The note's supervising provider, if one has been set last_modified_by_staff| [Staff](/sdk/data-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](/sdk/data-encounter)| location| [PracticeLocation](/sdk/data-practicelocation/#practicelocation)| The practice location associated with the note commands| QuerySet[[Command](/sdk/data-command)]| All commands associated with this note clipboards| QuerySet[[Clipboard](/sdk/data-clipboard/#clipboard)]| All clipboard commands recorded on this note custom_commands| QuerySet[[CustomCommand](/sdk/data-custom-command/#customcommand)]| All custom commands recorded on this note note_tasks| QuerySet[[NoteTask](/sdk/data-task)]| All tasks associated with this note metadata| QuerySet[NoteMetadata]| All metadata key-value pairs associated with this note lab_reviews| QuerySet[[LabReview](/sdk/data-labs/#labreview)]| All lab reviews associated with this note imaging_reviews| QuerySet[[ImagingReview](/sdk/data-imaging/#imagingreview)]| All imaging reviews associated with this note referral_reviews| QuerySet[[ReferralReview](/sdk/data-referral/#referralreview)]| All referral reviews associated with this note chart_section_reviews| QuerySet[[ChartSectionReview](/sdk/data-chart-section-review/#chartsectionreview)]| All chart section reviews associated with this note visual_exam_findings| QuerySet[[VisualExamFinding](/sdk/data-visual-exam-finding/#visualexamfinding)]| All visual exam findings associated with this note state_history| QuerySet[NoteStateChangeEvent]| The note's state-change audit history action_events| QuerySet[[NoteActionEvent](/sdk/data-fax/#noteactionevent)]| Faxes of this note, with their [delivery status](/sdk/data-fax/#delivery-status) current_state| CurrentNoteStateEvent| The note's current state event assessments| QuerySet[[Assessment](/sdk/data-assessment/#assessment)]| All assessments associated with this note goals| QuerySet[[Goal](/sdk/data-goal/#goal)]| All goals associated with this note updategoals| QuerySet[[UpdateGoal](/sdk/data-goal/#updategoal)]| All goal updates and closures recorded on this note instructions| QuerySet[[Instruction](/sdk/data-instruction/#instruction)]| All instructions associated with this note immunizations| QuerySet[[Immunization](/sdk/data-immunization/#immunization)]| All immunizations associated with this note claims| QuerySet[[Claim](/sdk/data-claim/#claim)]| All claims associated with this note (see the `get_claim()` method) letter| [Letter](/sdk/data-letter/#letter)| The letter associated with this note, if any referral_set| QuerySet[[Referral](/sdk/data-referral/#referral)]| All referrals associated with this note laborder_set| QuerySet[[LabOrder](/sdk/data-labs/#laborder)]| All lab orders associated with this note appointment_set| QuerySet[[Appointment](/sdk/data-appointment/#appointment)]| All appointments associated with this note education_material| QuerySet[[EducationalMaterial](/sdk/data-educational-material/#educationalmaterial)]| All educational materials recorded on this note procedures| QuerySet[[Procedure](/sdk/data-procedure/#procedure)]| All procedures recorded on this note family_histories| QuerySet[[FamilyHistory](/sdk/data-family-history/#familyhistory)]| All family history records recorded on this note plans| QuerySet[[Plan](/sdk/data-plan/#plan)]| All plans recorded on this note dme_orders| QuerySet[[DmeOrder](/sdk/data-dme/#dmeorder)]| All Order DME commands signed on this note follow_ups| QuerySet[[FollowUp](/sdk/data-follow-up/#followup)]| All follow-ups recorded on this note reasons_for_visit| QuerySet[[ReasonForVisit](/sdk/data-reason-for-visit/#reasonforvisit)]| All reasons for visit recorded on this note assessed_coding_gaps| QuerySet[[AssessCodingGapEvent](/sdk/data-coding-gap-event/#assesscodinggapevent)]| All coding gaps assessed on this note assessed_detected_issues| QuerySet[[ValidateCodingGapEvent](/sdk/data-coding-gap-event/#validatecodinggapevent)]| All coding gaps validated on this note created_detected_issues| QuerySet[[CreateCodingGapEvent](/sdk/data-coding-gap-event/#createcodinggapevent)]| All coding gaps created on this note deferred_detected_issues| QuerySet[[DeferCodingGapEvent](/sdk/data-coding-gap-event/#defercodinggapevent)]| All coding gaps deferred on this note removed_allergies| QuerySet[[RemoveAllergyEvent](/sdk/data-remove-allergy-event/#removeallergyevent)]| All allergies removed on this note removed_past_medical_history| QuerySet[[RemovePastMedicalHistoryEvent](/sdk/data-remove-past-medical-history-event/#removepastmedicalhistoryevent)]| All past medical history entries removed on this note resolved_conditions| QuerySet[[ResolveConditionEvent](/sdk/data-resolve-condition-event/#resolveconditionevent)]| All conditions resolved on this note histories_of_present_illness| QuerySet[[HistoryOfPresentIllness](/sdk/data-history-present-illness/#historyofpresentillness)]| All histories of present illness recorded on this note vital_sign_readings| QuerySet[[VitalSignReading](/sdk/data-vital-sign-reading/#vitalsignreading)]| All vital sign readings recorded on this note cancel_prescriptions| QuerySet[[CancelPrescription](/sdk/data-cancel-prescription/#cancelprescription)]| All prescription cancellations recorded on this note prescription_change_requests| QuerySet[[PrescriptionChangeRequest](/sdk/data-prescription-change-request/#prescriptionchangerequest)]| All pharmacy change requests recorded on this note prescription_change_responses| QuerySet[[PrescriptionChangeResponse](/sdk/data-prescription-change-response/#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](/sdk/data-appointment/#appointment)] ### NoteMetadata Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime note| Note key| String value| String ```python 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](/sdk/data-note/) originator| [CanvasUser](/sdk/data-canvasuser) state| [NoteState](/sdk/data-note/#notestates) note_state_document| String note_state_html| String > **Info:** The `note_state_document` and `note_state_html` fields hold the snapshot captured when a note is locked. The FHIR `DocumentReference` PDF content is copied from `note_state_document` at that same time, so all three are the copies consumers read. By default they include commands that were entered in error, which appear struck through in `note_state_html` as `Entered in Error/Removed on …` entries. When Canvas Support enables the opt-in setting (`EXCLUDE_ENTERED_IN_ERROR_FROM_NOTE_DOCUMENTS`, off by default) on an instance, these read copies instead omit entered-in-error commands — the field names and the `DocumentReference` schema are unchanged, only each copy's content differs. The full audit copy that keeps entered-in-error commands remains available from the note-footer PDF link served by `/api/NoteStateChangeEventDocument/{id}.pdf`. ### CurrentNoteStateEvent Field Name| Type ---|--- id| UUID dbid| Integer state| [NoteState](/sdk/data-note/#notestates) note| [Note](/sdk/data-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 --- # Observation Source: https://docs.canvasmedical.com/sdk/data-observation/ ## Introduction The `Observation` model represents measurements or assertions made about a patient, such as vital signs, lab results, or other clinical findings. ## Basic usage To get an observation by identifier, use the `get` method on the `Observation` model manager: ```python from canvas_sdk.v1.data.observation import Observation observation = Observation.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the observations for a patient can be accessed with the `observations` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") observations = patient.observations.all() ``` If you have a patient ID, you can get the observations for the patient with the `for_patient` method on the `Observation` model manager: ```python from canvas_sdk.v1.data.observation import Observation patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" observations = Observation.objects.for_patient(patient_id) ``` ## Accessing questionnaire scores The `QuestionnaireResult` model, and the `score` on it, is not exposed in the SDK data module. When Canvas scores a questionnaire, it records the score on an `Observation` created alongside the result, so you read scores through the patient's observations. Each scored questionnaire produces one observation: Attribute| Value ---|--- `name`| The questionnaire's name, such as `PHQ-9` `value`| The score, as a string `category`| `survey` `note_id`| The `dbid` of the note the questionnaire was completed in `codings`| The questionnaire's scoring code, such as LOINC `44261-6` for the PHQ-9 total score. A questionnaire that could not be scored, such as one left incomplete, has no coding. To find a patient's scores for one questionnaire, filter on its coding. `effective_datetime` is not set on these observations, so order by `created`: ```python from canvas_sdk.v1.data.observation import Observation phq9_scores = ( Observation.objects.for_patient("b80b1cdc2e6a4aca90ccebc02e683f35") .committed() .filter(category="survey", codings__system="http://loinc.org", codings__code="44261-6") .order_by("-created") ) for observation in phq9_scores: print(observation.name, observation.value, observation.created) ``` To get the scores recorded in a specific note, filter on `note_id` with the note's `dbid` instead of the coding. ## Codings The codings for an observation can be accessed with the `codings` attribute on an `Observation` object: ```python from canvas_sdk.v1.data.observation import Observation from logger import log observation = Observation.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") for coding in observation.codings.all(): log.info(f"system: {coding.system}") log.info(f"code: {coding.code}") log.info(f"display: {coding.display}") ``` ## Components The components for an observation can be accessed with the `components` attribute on an `Observation` object: ```python from canvas_sdk.v1.data.observation import Observation from logger import log observation = Observation.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") for component in observation.components.all(): log.info(f"name: {component.name}") log.info(f"value: {component.value_quantity}") log.info(f"unit: {component.value_quantity_unit}") ``` ### Component codings Component codings can be accessed similarly to codings on the observation, by using the `codings` attribute on an `ObservationComponent` object. ## Filtering Observations can be filtered by any attribute that exists on the model. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.observation import Observation observations = Observation.objects.filter(effective_datetime__gte="2024-11-20") ``` ### By ValueSet See [Value Sets](/sdk/data-value-sets/) for the library of built-in value sets and how to create your own. Filtering by ValueSet works a little differently. The `find` method on the model manager is used to perform `ValueSet` filtering: ```python from canvas_sdk.v1.data.observation import Observation from canvas_sdk.value_set.v2022.physical_exam import Weight observations = Observation.objects.find(Weight) ``` ## Attributes ### Observation Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) is_member_of| Observation category| String (comma-separated list of categories units| String value| String note_id| Integer name| String effective_datetime| DateTime codings| ObservationCoding[] members| Observation[] components| ObservationComponent[] value_codings| ObservationValueCoding[] ### ObservationCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean observation| Observation ### ObservationComponent Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime observation| Observation value_quantity| String value_quantity_unit| String name| String codings| ObservationComponentCoding[] ### ObservationComponentCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean observation_component| ObservationComponent ### ObservationValueCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean observation| Observation --- # Organization Source: https://docs.canvasmedical.com/sdk/data-organization/ ## Introduction The `Organization` model represents the overall Organization in a Canvas EMR instance. An `Organization` can have multiple related [Practice Locations](/sdk/data-practicelocation). ## Basic usage Canvas instances can contain only a single `Organization` entry. To retrieve the `Organization` entry, you can either query by the organization's name: ```python from canvas_sdk.v1.data.organization import Organization organization = Organization.objects.get(full_name="Medical Organization") ``` Or since there will only be one `Organization` in an instance, it can also be fetched by using the `first` method: ```python from canvas_sdk.v1.data.organization import Organization organization = Organization.objects.first() ``` ## Attributes ### Organization Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime full_name| String short_name| String subdomain| String logo_url| String background_image_url| String background_gradient| String active| Boolean tax_id| String tax_id_type| [TaxIDType](/sdk/data-enumeration-types/#taxidtype) group_npi_number| String group_taxonomy_number| String include_zz_qualifier| Boolean main_location| [PracticeLocation](/sdk/data-practicelocation/#practicelocation) practice_locations| QuerySet[[PracticeLocation](/sdk/data-practicelocation/#practicelocation)] addresses| QuerySet[OrganizationAddress] telecom| QuerySet[OrganizationContactPoint] business_lines| QuerySet[[BusinessLine](/sdk/data-business-line/#businessline)] ## OrganizationAddress The `OrganizationAddress` model represents a physical or mailing address associated with an Organization. Multiple addresses can be linked to a single Organization, each with its own type and details. ### Attributes Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime organization| Organization use| [AddressUseWithBilling](/sdk/data-enumeration-types/#addressusewithbilling) type| [AddressType](/sdk/data-enumeration-types/#addresstype) longitude| Float latitude| Float start| Date end| Date country| String state| [AddressState](/sdk/data-enumeration-types/#addressstate) address_search_index| String line1| String line2| String city| String district| String state_code| String postal_code| String ## OrganizationContactPoint The `OrganizationContactPoint` model represents a contact method (such as phone, email, or fax) for an Organization. Multiple contact points can be associated with a single Organization, each with its own type, use, and status. ### Attributes Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime organization| Organization system| [ContactPointSystem](/sdk/data-enumeration-types/#contactpointsystem) value| String use| [ContactPointUse](/sdk/data-enumeration-types/#contactpointuse) use_notes| String rank| Integer state| [ContactPointState](/sdk/data-enumeration-types/#contactpointstate) --- # OrganizationalEntity Source: https://docs.canvasmedical.com/sdk/data-organizational-entity/ ## Introduction The `OrganizationalEntity` model represents an external entity that Canvas references through a generic relation — for example, the [ServiceProvider](/sdk/data-serviceprovider/#service-provider) backing a patient's external care team member. Its `type` indicates which kind of entity it points at, and the `content_type` and `object_id` fields identify the specific record. The most common use is reaching the external members of a patient's care team. A [CareTeamMembership](/sdk/data-care-team/#careteammembership) with no `staff` is an external member, and its `organizational_entity` links to the `OrganizationalEntity` describing the external provider. ## Basic usage When an `OrganizationalEntity` has a `type` of `Service Provider`, its `service_provider` property resolves to the linked [ServiceProvider](/sdk/data-serviceprovider/#service-provider), giving you access to the provider's contact details — such as `business_fax` — without leaving the plugin: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="a74592ae8a6c4d0ebe0799d3fb3713d1") external_member = patient.care_team_memberships.filter(staff__isnull=True).first() entity = external_member.organizational_entity if entity and entity.service_provider: print(entity.service_provider.business_fax) ``` For entities of any other `type`, the `service_provider` property returns `None`. ## Attributes ### OrganizationalEntity Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| content_type| [ContentType](/sdk/data-content-type/#contenttype)| object_id| Integer| name| String| active| Boolean| type| OrganizationalEntityType| service_provider| [ServiceProvider](/sdk/data-serviceprovider/#service-provider) | `None` (computed)| The linked `ServiceProvider` when `type` is `Service Provider`; otherwise `None`. ## Enumeration types ### OrganizationalEntityType Value| Label ---|--- Transactor| Transactor Business Entity| Business Entity Vendor| Vendor Service Provider| Service Provider --- # PatientAdministrativeDocument Source: https://docs.canvasmedical.com/sdk/data-patient-administrative-document/ The `PatientAdministrativeDocument` model represents patient-facing administrative documents: prior authorizations, advance directives and beneficiary notices, signed consent forms and agreements, insurance and prescription cards, driver's licenses, intake forms, releases of information, powers of attorney, and workers' compensation attachments. Each carries a document file and an optional `DocumentCoding`. A signed consent form is one of these records: `patient_consents` lists the [PatientConsent](/sdk/data-patient-consent/#signed-consent-documents) records it was signed for. The blank template the patient was sent lives on the consent itself, not here. ## Basic Usage ```python from canvas_sdk.v1.data import PatientAdministrativeDocument # Get all administrative documents documents = PatientAdministrativeDocument.objects.all() # Get a specific record by its id document = PatientAdministrativeDocument.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") # Get a patient's administrative documents patient_documents = PatientAdministrativeDocument.objects.filter( patient__id="1eed3ea2a8d546a1b681a2a45de1d790" ) ``` ## Filtering Patient administrative documents can be filtered by any attribute that exists on the model. ### By patient ```python from canvas_sdk.v1.data import PatientAdministrativeDocument, Patient patient = Patient.objects.get(id="b80b1cdc2e6a4aca90ccebc02e683f35") documents = PatientAdministrativeDocument.objects.filter(patient=patient) ``` ## Accessing the document file The `document_url` property returns a presigned S3 URL for securely accessing the document file, or `None` when no file is present. ```python from canvas_sdk.v1.data import PatientAdministrativeDocument document = PatientAdministrativeDocument.objects.exclude(document="").first() # Returns a presigned S3 URL (valid for 1 hour) url = document.document_url ``` ## Review assignment A document is assigned for review to staff members or to a team. `reviewers` holds the assigned staff members, and `team` holds the assigned team, or `None` when no team is assigned. ```python from canvas_sdk.v1.data import PatientAdministrativeDocument, Staff document = PatientAdministrativeDocument.objects.get(id="c1a5a35a-4ee2-4a0e-85c0-21739dc8c4a8") # The staff members and the team the document is assigned to reviewers = document.reviewers.all() team = document.team # Every document assigned to this staff member staff = Staff.objects.get(id="4150cd20de8a470aa570a852859ac87e") assigned = PatientAdministrativeDocument.objects.filter(reviewers__id=staff.id) ``` ## The document reference Each record also has a [DocumentReference](/sdk/data-document-reference/#the-related-object) pointing back at it — the record that carries the document's coding, category and status, and that represents it in the FHIR API. `document_url` above is the direct route to the file itself; reach for the document reference when you want that surrounding metadata. Resolve the [ContentType](/sdk/data-content-type/) at runtime from its stable `app_label` and `model` — never hardcode the per-environment `dbid` — and match `object_id` against the record's `dbid`: ```python from canvas_sdk.v1.data import ContentType, DocumentReference, PatientAdministrativeDocument record = PatientAdministrativeDocument.objects.get( id="d2194110-5c9a-4842-8733-ef09ea5ead11" ) content_type = ContentType.objects.filter( app_label="api", model="patientadministrativedocument" ).first() document = DocumentReference.objects.filter( content_type=content_type, object_id=record.dbid ).first() ``` > **Info:** `object_id` holds the related record's integer `dbid`, not its UUID `id`. ## Document codings The `code` field comes from the document's type, which is drawn from a fixed list rather than set freely — either the type selected in Data Integration, or, when a document is created through the FHIR [DocumentReference](/api/documentreference/) endpoint, the LOINC code supplied in `type.coding`, which must match one of the codes below. Every coding uses the LOINC system (`http://loinc.org`). The document types stored as patient administrative documents are: Document type| Code| Display ---|---|--- Advance Beneficiary Notice| 53243-2| Advanced beneficiary notice Advance Directive| 42348-3| Advance directives Commercial Driver License| 53245-7| Driver license Insurance Card Image| 64290-0| Health insurance card Insurer Prior Authorization| 52034-6| Payer letter Patient Agreement| 80570-5| Agreement Patient Consent Documents| 59284-0| Consent Document Power of Attorney| 64298-3| Power of attorney Provider Order| 46209-3| Provider orders Release of Information Request| 101904-1| Release of Information request Uncategorized Administrative Document| 51851-4| Administrative note Workers Compensation Documents| 52070-0| Workers compensation attachment Disability Form| —| none Handicap Parking Permit| —| none Medicaid Documents| —| none Patient Assistance Documents| —| none Patient Intake Form| —| none Prescription Card Documents| —| none > **Warning:** Six of these document types have no coding assigned, so their `code` is `None`. Filtering on `code` silently excludes them — check for a null `code` if you need to catch every administrative document. Because the FHIR endpoint identifies a document's type by its LOINC code, these six can only be created through Data Integration. Clinical document types are stored as [UncategorizedClinicalDocument](/sdk/data-uncategorized-clinical-document/) instead. Lab reports, imaging reports and specialist consult reports have their own models, so their codings never appear here. ## Attributes ### PatientAdministrativeDocument Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| patient| [Patient](/sdk/data-patient/#patient)| originator| [CanvasUser](/sdk/data-canvasuser)| assigned_by| [CanvasUser](/sdk/data-canvasuser)| team| [Team](/sdk/data-team/#team)| reviewers| [Staff](/sdk/data-staff/#staff)[]| integration_task_review| [IntegrationTaskReview](/sdk/data-integration-task/#integrationtaskreview)| code| DocumentCoding| name| String| review_mode| [DocumentReviewMode](/sdk/data-enumeration-types/#documentreviewmode)| junked| Boolean| assigned_date| DateTime| team_assigned_date| DateTime| original_date| Date| comment| String| priority| Boolean| document| String| document_url| String (computed)| Presigned S3 URL or None patient_consents| QuerySet[[PatientConsent](/sdk/data-patient-consent/#patientconsent)]| The consents this document is a signed copy of ### DocumentCoding A coding entry representing the type of document. Also used by [UncategorizedClinicalDocument](/sdk/data-uncategorized-clinical-document/#uncategorizedclinicaldocument). Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean --- # PatientConsent Source: https://docs.canvasmedical.com/sdk/data-patient-consent/ ## Introduction The `PatientConsent` model represents documented patient consents in Canvas that ensure legal compliance and protect patient rights. Each `PatientConsent` is linked to a `Patient`, has a category (which is a `PatientConsentCoding`), and optionally a rejection reason (which is a `PatientConsentRejectionCoding`). ## Usage The `PatientConsent` model can be used to find all of the patient consents for a given patient and organization: ```python >>> from canvas_sdk.v1.data import PatientConsent, Patient, Organization >>> patient_1 = Patient.objects.get(id="aebe4d3f5d18410388dc69c4b5169fc3") >>> organization_1 = Organization.objects.first() >>> patient_1_consents = PatientConsent.objects.filter(patient=patient_1, organization=organization_1) >>> print([consent.category.display for consent in patient_1_consents]) ['Surgical Consent Form', 'Telehealth', 'HIPAA'] ``` You can also access a patient's consents from the `Patient` model: ```python >>> from canvas_sdk.v1.data import PatientConsent, Patient >>> patient_1 = Patient.objects.get(id="aebe4d3f5d18410388dc69c4b5169fc3") >>> patient_1_consents = patient_1.patient_consent.all() >>> print([consent.category.display for consent in patient_1_consents]) ['Surgical Consent Form', 'Telehealth', 'HIPAA'] ``` And you can also access all of the PatientConsents for a given PatientConsentCoding (aka category): ```python >>> from canvas_sdk.v1.data import PatientConsentCoding >>> coding = PatientConsentCoding.objects.get(code='59284-0', system='LOINC') >>> consents = coding.patient_consent.all() >>> print([consent.state for consent in consents]) ['accepted', 'accepted_via_patient_portal', 'rejected'] ``` Each `PatientConsentCoding` has a `document` field containing the URL to the consent template document: ```python >>> from canvas_sdk.v1.data import PatientConsentCoding >>> coding = PatientConsentCoding.objects.first() >>> print(coding.document) 'consent_templates/hipaa_consent.pdf' ``` ## Accessing Document Files The `document_url` property returns a presigned S3 URL for securely accessing the blank consent template — the form you send to a patient to collect their consent. The copy the patient signs and returns is a separate record; see Signed consent documents. ```python from canvas_sdk.v1.data import PatientConsentCoding consent_coding = PatientConsentCoding.objects.first() # Returns a presigned S3 URL (valid for 1 hour) url = consent_coding.document_url ``` ## Signed consent documents The `PatientConsentCoding.document` above is the blank **template** sent to the patient. The **signed** documents the patient completes and returns are [PatientAdministrativeDocument](/sdk/data-patient-administrative-document/) records, reachable from the consent through the `documents` relation: ```python from canvas_sdk.v1.data import PatientConsent consent = PatientConsent.objects.get(id="8a2b1c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d") # Every signed document attached to this consent. signed_documents = consent.documents.all() # The current signed document (the most recent non-junked one), or None. current = consent.active_document ``` Each signed document's FHIR [DocumentReference](/sdk/data-document-reference/) is reachable through `document_references`, so a plugin can read the reference (and its `related_object`) in-process without a FHIR call: ```python from canvas_sdk.v1.data import PatientConsent consent = PatientConsent.objects.get(id="8a2b1c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d") for reference in consent.document_references: url = reference.document_url ``` ## Attributes ### PatientConsent Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| patient| [Patient](/sdk/data-patient)| category| PatientConsentCoding| state| PatientConsentStatus| effective_date| DateTime| expired_date| DateTime| rejection_reason| PatientConsentRejectionCoding| originator| [CanvasUser](/sdk/data-canvasuser)| documents| QuerySet[[PatientAdministrativeDocument](/sdk/data-patient-administrative-document/)]| The signed consent documents active_document| [PatientAdministrativeDocument](/sdk/data-patient-administrative-document/) (computed)| The current signed document, or `None` document_references| QuerySet[[DocumentReference](/sdk/data-document-reference/)] (computed)| References for the signed documents ### PatientConsentCoding Field Name| Type| Description ---|---|--- dbid| Integer| created| DateTime| modified| DateTime| system| String| version| String| code| String| display| String| user_selected| Boolean| expiration_rule| PatientConsentExpirationRule| is_mandatory| Boolean| is_proof_required| Boolean| show_in_patient_portal| Boolean| summary| String| document| String| document_url| String (computed)| Presigned S3 URL patient_consent| QuerySet[PatientConsent]| ### PatientConsentRejectionCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean patient_consents| QuerySet[PatientConsent] ## Enumeration types ### PatientConsentStatus Member| Value| Label ---|---|--- `ACCEPTED`| accepted| Accepted `ACCEPTED_VIA_PORTAL`| accepted_via_patient_portal| Accepted Via Patient Portal `REJECTED`| rejected| Rejected `REJECTED_VIA_PORTAL`| rejected_via_patient_portal| Rejected Via Patient Portal ### PatientConsentExpirationRule Value| Label ---|--- never| Never in_one_year| In one year end_of_year| End of year --- # Patient Group Source: https://docs.canvasmedical.com/sdk/data-patient-group/ ## Introduction The `PatientGroup` model represents a named collection of patients. Patients are associated with a group through the `PatientGroupMember` model, which tracks membership along with start/end dates and active status. ## Basic usage To get a patient group by identifier, use the `get` method on the `PatientGroup` model manager: ```python from canvas_sdk.v1.data.patient_group import PatientGroup group = PatientGroup.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` To get the members of a group: ```python from canvas_sdk.v1.data.patient_group import PatientGroup from logger import log group = PatientGroup.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") for patient in group.members.all(): log.info(f"Patient: {patient.id}") ``` If you have a patient object, the groups that a patient belongs to can be accessed with the `patient_groups` attribute on a [Patient](/sdk/data-patient/#patient) object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="a74592ae8a6c4d0ebe0799d3fb3713d1") groups = patient.patient_groups.all() ``` ## Membership The `PatientGroupMember` model represents a patient's membership in a group. To access the membership records for a group: ```python from canvas_sdk.v1.data.patient_group import PatientGroup, PatientGroupMember from logger import log group = PatientGroup.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") # Get active members active_members = PatientGroupMember.objects.filter(patient_group=group, active=True) for membership in active_members: log.info(f"Patient: {membership.member.id}, Start: {membership.start_date}") ``` ## Filtering Patient groups and memberships can be filtered by any attribute that exists on the model. ### By name ```python from canvas_sdk.v1.data.patient_group import PatientGroup groups = PatientGroup.objects.filter(name="Diabetes Management") ``` ### By active membership ```python from canvas_sdk.v1.data.patient_group import PatientGroupMember active_memberships = PatientGroupMember.objects.filter(active=True, patient_group__name="Diabetes Management") ``` ## Attributes ### PatientGroup Field Name| Type ---|--- id| UUID name| String members| [Patient](/sdk/data-patient/)[] created| DateTime modified| DateTime patientgroupmember_set| PatientGroupMember[] ### PatientGroupMember Field Name| Type ---|--- created| DateTime modified| DateTime patient_group| PatientGroup member| [Patient](/sdk/data-patient/) start_date| DateTime end_date| DateTime (nullable) locked| Boolean active| Boolean --- # Patient Source: https://docs.canvasmedical.com/sdk/data-patient/ ## Introduction The `Patient` model represents an individual receiving care or other health-related services. ## Basic usage To get a patient by identifier, use the `get` method on the `Patient` model manager: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="b80b1cdc2e6a4aca90ccebc02e683f35") ``` ## Filtering Patients can be filtered by any attribute that exists on the model. Filtering for patients is done with the `filter` method on the `Patient` model manager. ### By attribute Specify attributes with `filter` to filter by those attributes: ```python from canvas_sdk.v1.data.patient import Patient patients = Patient.objects.filter(first_name="Bob", last_name="Loblaw", birth_date="1960-09-22") ``` ### Retrieving only committed, recorded data To restrict a patient's clinical data to committed records, use the queryset's `committed()` method. It keeps records with a non-null `committer` and a null `entered_in_error`, and the model's default manager already excludes deleted records. For example, for a patient's observations: ```python from canvas_sdk.v1.data.observation import Observation observations = Observation.objects.for_patient(patient.id).committed() ``` Most clinical data models provide `committed()`. On a model that records a committer without providing it, such as [`Command`](/sdk/data-command/), filter on `committer_id__isnull=False, entered_in_error_id__isnull=True` directly. ## Accessing the patient photo The `photo_url` property returns a presigned S3 URL for securely accessing the patient's uploaded avatar photo. If the patient has no uploaded avatar, the property returns a default avatar URL instead — so the value is always safe to render without a null check. ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="d7af3e356368446c85b40a5d6ff7288e") # Returns a presigned S3 URL (valid for 1 hour), or the default avatar URL when no photo is on file url = patient.photo_url ``` If you need the underlying `PatientPhoto` record (for example, to read its `title`, or the stored file key with `url.name`), use the `photo` property: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="d7af3e356368446c85b40a5d6ff7288e") photo = patient.photo # PatientPhoto or None if photo: print(photo.title) ``` ## Reading uploaded images and PDFs To read uploaded files (such as insurance card images or scanned PDFs) programmatically, use the file-upload data models, which expose the underlying S3 link rather than requiring direct bucket/AWS credentials: - [`DocumentReference`](/sdk/data-document-reference/) — uploaded documents - [`Snapshot` / `SnapshotImage`](/sdk/data-snapshot/) — snapshot images - `PatientIdentificationCard` — insurance card and ID card images (`image_url` property) - [`MessageAttachment.file_url`](/sdk/data-message/#messageattachment) — files attached to messages ## Accessing educational materials If you have a `Patient` object, the educational materials recorded on their notes can be accessed with the `education_material` reverse relation: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="d7af3e356368446c85b40a5d6ff7288e") educational_materials = patient.education_material.all() ``` ## Detecting patient portal registration To detect whether a patient has registered for the patient portal, use the `is_portal_registered` boolean on the patient's [`CanvasUser`](/sdk/data-canvasuser) record (via the `user` relation). Patient portal verification is a separate process from contact-point verification, so check this boolean rather than `PatientContactPoint.last_verified` for portal registration: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="d7af3e356368446c85b40a5d6ff7288e") registered = patient.user.is_portal_registered ``` ## Attributes ### Patient Field Name| Type| Description ---|---|--- id| String| dbid| Integer| first_name| String| last_name| String| birth_date| Date| sex_at_birth| SexAtBirth| created| DateTime| modified| DateTime| prefix| String| suffix| String| middle_name| String| maiden_name| String| nickname| String| sexual_orientation_term| String| sexual_orientation_code| String| gender_identity_term| String| gender_identity_code| String| preferred_pronouns| String| biological_race_codes| Array[String]| cultural_ethnicity_codes| Array[String]| last_known_timezone| String| mrn| String| active| Boolean| deceased| Boolean| deceased_datetime| DateTime| deceased_cause| String| deceased_comment| String| other_gender_description| String| social_security_number| String| administrative_note| String| clinical_note| String| mothers_maiden_name| String| multiple_birth_indicator| Boolean| birth_order| Integer| default_location_id| Integer| default_provider_id| Integer| addresses| PatientAddress[]| allergy_intolerances| [AllergyIntolerance](/sdk/data-allergy-intolerance/#allergyintolerance)[]| assessed_coding_gaps| [AssessCodingGapEvent](/sdk/data-coding-gap-event/#assesscodinggapevent)[]| assessed_detected_issues| [ValidateCodingGapEvent](/sdk/data-coding-gap-event/#validatecodinggapevent)[]| billing_line_items| [BillingLineItem](/sdk/data-billing-line-item/)| business_line| [BusinessLine](/sdk/data-business-line/)| care_team_memberships| [CareTeamMembership](/sdk/data-care-team/#careteammembership)[]| change_medications| [ChangeMedication](/sdk/data-change-medication/#changemedication)[]| clipboards| [Clipboard](/sdk/data-clipboard/#clipboard)[]| conditions| [Condition](/sdk/data-condition/#condition)[]| coverages| [Coverage](/sdk/data-coverage/#coverage)[]| created_detected_issues| [CreateCodingGapEvent](/sdk/data-coding-gap-event/#createcodinggapevent)[]| custom_commands| [CustomCommand](/sdk/data-custom-command/#customcommand)[]| deferred_detected_issues| [DeferCodingGapEvent](/sdk/data-coding-gap-event/#defercodinggapevent)[]| dependent_coverages| [Coverage](/sdk/data-coverage/#coverage)[]| detected_issues| [DetectedIssue](/sdk/data-detected-issue/#detectedissue)[]| devices| [Device](/sdk/data-device/#device)[]| dme_orders| [DmeOrder](/sdk/data-dme/#dmeorder)[]| external_identifiers| PatientExternalIdentifier[]| identification_cards| PatientIdentificationCard[]| imaging_orders| [ImagingOrder](/sdk/data-imaging/#imagingorder)[]| imaging_results| [ImagingReport](/sdk/data-imaging/#imagingreport)[]| imaging_reviews| [ImagingReview](/sdk/data-imaging/#imagingreview)[]| interviews| [Interview](/sdk/data-questionnaire/#interview)[]| lab_orders| [LabOrder](/sdk/data-labs/#laborder)[]| lab_reports| [LabReport](/sdk/data-labs/#labreport)[]| lab_reviews| [LabReview](/sdk/data-labs/#labreview)[]| medications| [Medication](/sdk/data-medication/#medication)[]| metadata| PatientMetadata[]| observations| [Observation](/sdk/data-observation/#observation)[]| payment_cards| [PaymentCard](/sdk/data-payment-card/#paymentcard)[]| photos| PatientPhoto[]| preferred_pharmacy| JSON| preferred_pharmacies| JSON| protocol_overrides| [ProtocolOverride](/sdk/data-protocol-override/#protocoloverride)[]| settings| PatientSetting| subscribed_coverages| [Coverage](/sdk/data-coverage/#coverage)[]| tasks| [Task](/sdk/data-task/#task)[]| telecom| PatientContactPoint[]| contacts| PatientContactPerson[]| related_contacts| PatientContactPerson[]| Contacts on _other_ patients that reference this one user| [CanvasUser](/sdk/data-canvasuser/)[]| patient_groups| [PatientGroup](/sdk/data-patient-group/)[]| chart_section_reviews| [ChartSectionReview](/sdk/data-chart-section-review/#chartsectionreview)[]| visual_exam_findings| [VisualExamFinding](/sdk/data-visual-exam-finding/#visualexamfinding)[]| vital_sign_readings| [VitalSignReading](/sdk/data-vital-sign-reading/#vitalsignreading)[]| assessments| [Assessment](/sdk/data-assessment/#assessment)[]| patient_visits| [ExternalVisit](/sdk/data-external-event/#externalvisit)[]| patient_events| [ExternalEvent](/sdk/data-external-event/#externalevent)[]| medication_statements| [MedicationStatement](/sdk/data-medication-statement/#medicationstatement)[]| diagnostic_reports| DiagnosticReport[]| medication_history_medications| [MedicationHistoryMedication](/sdk/data-medication-history/#medicationhistorymedication)[]| medication_history_responses| [MedicationHistoryResponse](/sdk/data-medication-history/#medicationhistoryresponse)[]| payments| [BulkPatientPosting](/sdk/data-posting/#bulkpatientposting)[]| protocol_currents| [ProtocolCurrent](/sdk/data-protocol-current/)[]| stopped_medications| [StopMedicationEvent](/sdk/data-stop-medication-event/#stopmedicationevent)[]| banner_alerts| [BannerAlert](/sdk/data-banner-alert/#banneralert)[]| immunizations| [Immunization](/sdk/data-immunization/#immunization)[]| immunization_statements| [ImmunizationStatement](/sdk/data-immunization/#immunizationstatement)[]| integration_tasks| [IntegrationTask](/sdk/data-integration-task/#integrationtask)[]| installment_plans| [InstallmentPlan](/sdk/data-claim/#installmentplan)[]| uncategorized_clinical_document_reviews| [UncategorizedClinicalDocumentReview](/sdk/data-uncategorized-clinical-document/#uncategorizedclinicaldocumentreview)[]| patient_consent| [PatientConsent](/sdk/data-patient-consent/#patientconsent)[]| goals| [Goal](/sdk/data-goal/#goal)[]| updategoals| [UpdateGoal](/sdk/data-goal/#updategoal)[]| instructions| [Instruction](/sdk/data-instruction/#instruction)[]| appointments| [Appointment](/sdk/data-appointment/#appointment)[]| notes| [Note](/sdk/data-note/#note)[]| prescriptions| [Prescription](/sdk/data-prescription/#prescription)[]| refill_requests| [RefillRequest](/sdk/data-refill-request/#refillrequest)[]| referral_reviews| [ReferralReview](/sdk/data-referral/#referralreview)[]| referral_reports| [ReferralReport](/sdk/data-referral/#referralreport)[]| invoices| Invoice[]| education_material| [EducationalMaterial](/sdk/data-educational-material/#educationalmaterial)[]| procedures| [Procedure](/sdk/data-procedure/#procedure)[]| family_histories| [FamilyHistory](/sdk/data-family-history/#familyhistory)[]| histories_of_present_illness| [HistoryOfPresentIllness](/sdk/data-history-present-illness/#historyofpresentillness)[]| plans| [Plan](/sdk/data-plan/#plan)[]| follow_ups| [FollowUp](/sdk/data-follow-up/#followup)[]| reasons_for_visit| [ReasonForVisit](/sdk/data-reason-for-visit/#reasonforvisit)[]| removed_allergies| [RemoveAllergyEvent](/sdk/data-remove-allergy-event/#removeallergyevent)[]| removed_past_medical_history| [RemovePastMedicalHistoryEvent](/sdk/data-remove-past-medical-history-event/#removepastmedicalhistoryevent)[]| resolved_conditions| [ResolveConditionEvent](/sdk/data-resolve-condition-event/#resolveconditionevent)[]| cancel_prescriptions| [CancelPrescription](/sdk/data-cancel-prescription/#cancelprescription)[]| cancel_prescription_responses| [CancelPrescriptionResponse](/sdk/data-cancel-prescription-response/#cancelprescriptionresponse)[]| prescription_change_requests| [PrescriptionChangeRequest](/sdk/data-prescription-change-request/#prescriptionchangerequest)[]| prescription_change_responses| [PrescriptionChangeResponse](/sdk/data-prescription-change-response/#prescriptionchangeresponse)[]| full_name| String (computed)| The full name of the patient, combining first, middle, and last names preferred_pharmacy| Dict (computed)| The patient's preferred pharmacy for medication fulfillment preferred_full_name| String (computed)| The patient's preferred full name, if different from the legal name preferred_first_name| String (computed)| The patient's preferred first name, if different from the legal first name primary_phone_number| PatientContactPoint (computed)| The patient's primary contact number photo| PatientPhoto (computed)| The patient's first uploaded avatar, if any photo_url| String (computed)| A presigned URL for the patient's avatar photo, or the default avatar URL when no photo is set ### PatientAddress Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime line1| String line2| String city| String district| String state_code| String postal_code| String use| [AddressUse](/sdk/data-enumeration-types/#addressuse) type| [AddressType](/sdk/data-enumeration-types/#addresstype) longitude| Float latitude| Float start| Date end| Date country| String state| [AddressState](/sdk/data-enumeration-types/#addressstate) patient| Patient ```python from canvas_sdk.v1.data.patient import Patient from logger import log patient_id = "d7af3e356368446c85b40a5d6ff7288e" patient = Patient.objects.get(id=patient_id) patient_addresses = patient.addresses.all() for addr in patient_addresses: log.info(f"Patient address: {addr.city}, {addr.state_code}, {addr.postal_code}") # Seattle, WA, 98118 ``` ### PatientContactPoint Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime system| [ContactPointSystem](/sdk/data-enumeration-types/#contactpointsystem) value| String use| String use_notes| String rank| Integer state| [ContactPointState](/sdk/data-enumeration-types/#contactpointstate) patient| Patient has_consent| Boolean last_verified| DateTime verification_token| String opted_out| Boolean To fetch a patient's contact points, query the patient's `telecom` relation. Use `.all()` for every contact point, or `.filter()` for a subset such as phone numbers: ```python from canvas_sdk.v1.data.patient import Patient from logger import log patient_id = "d7af3e356368446c85b40a5d6ff7288e" patient = Patient.objects.get(id=patient_id) # All contact points patient_contacts = patient.telecom.all() # Just the phone numbers phone_contacts = patient.telecom.filter(system="phone") for contact in patient_contacts: log.info(f"Patient contact: {contact.system} - {contact.value}") # phone - 5555555555 ``` ### PatientExternalIdentifier Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime patient| Patient use| String identifier_type| String system| String value| String issued_date| Date expiration_date| Date ```python from canvas_sdk.v1.data.patient import Patient from logger import log patient_id = "d7af3e356368446c85b40a5d6ff7288e" patient = Patient.objects.get(id=patient_id) patient_external_identifiers = patient.external_identifiers.all() for identifier in patient_external_identifiers: log.info(f"Patient external identifier: {identifier.system}, {identifier.value}") # https://www.example.com - abc123 ``` ### PatientSetting Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime patient| Patient name| String value| JSON #### Setting names A patient carries at most one setting per `name`. Canvas maintains these: Name| Holds ---|--- `pharmacy`| The patient's preferred pharmacies. Read this through `preferred_pharmacies`, which normalizes the older single-pharmacy shape into a list `preferredSchedulingTimezone`| The timezone to schedule the patient in, for example `America/New_York` Because `value` is JSON, its shape differs from one setting to the next. Use `get_setting` to read one by name. It returns `None` when the patient has no setting by that name, so a missing setting and a stored empty value are distinguishable. ```python from canvas_sdk.v1.data.patient import Patient from logger import log patient_id = "d7af3e356368446c85b40a5d6ff7288e" patient = Patient.objects.get(id=patient_id) log.info(patient.get_setting("preferredSchedulingTimezone")) # America/New_York log.info(patient.get_setting("unset-setting-name")) # None ``` ### PatientMetadata Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime patient| Patient key| String value| String ```python from canvas_sdk.v1.data.patient import Patient from logger import log patient_id = "d7af3e356368446c85b40a5d6ff7288e" patient = Patient.objects.get(id=patient_id) patient_metadata = patient.metadata.all() for metadata in patient_metadata: log.info(f"Patient metadata: {metadata.key}, {metadata.value}") # favorite_color - red ``` ### PatientPhoto Represents a patient's uploaded avatar photo. Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime patient| Patient url| File (S3 key) title| String ```python from canvas_sdk.v1.data.patient import Patient from logger import log patient = Patient.objects.get(id="d7af3e356368446c85b40a5d6ff7288e") for photo in patient.photos.all(): log.info(f"Photo: {photo.title}, stored at: {photo.url.name}") ``` ### PatientIdentificationCard Represents a patient identification card image (e.g., driver's license, insurance card). To create, update, or delete identification cards from a plugin, use the [PatientIdentificationCard effect](/sdk/effect-patient-identification-card/). Field Name| Type| Description ---|---|--- dbid| Integer| created| DateTime| modified| DateTime| patient| Patient| image| File (S3 key)| title| String| active| Boolean| image_url| String (computed)| Presigned S3 URL ```python from canvas_sdk.v1.data.patient import Patient from logger import log patient = Patient.objects.get(id="d7af3e356368446c85b40a5d6ff7288e") for card in patient.identification_cards.filter(active=True): log.info(f"ID card: {card.title}, URL: {card.image_url}") ``` ### PatientFacilityAddress Field Name| Type ---|--- patientaddress| PatientAddress facility| Facility room_number| String ### PatientContactPerson One of the patient's contacts — an emergency contact, next-of-kin, or other related person. A contact either holds the person's details directly, or references another Canvas patient through `related_patient`; when it does, that patient's own details supersede the values stored here. `id` is the value the [Patient effect](/sdk/effect-patient/#managing-patient-contacts) takes as `contact_identifier` when modifying or removing a contact. Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime patient| Patient name| String phone_number| String email| String comments| String related_patient| Patient categories| PatientContactCategory[] ```python from canvas_sdk.v1.data import PatientContactPerson from logger import log contacts = PatientContactPerson.objects.filter( patient__id="d7af3e356368446c85b40a5d6ff7288e" ).select_related("related_patient").prefetch_related("categories__category") for contact in contacts: who = contact.related_patient.first_name if contact.related_patient else contact.name codings = ", ".join(link.category.code for link in contact.categories.all()) log.info(f"Contact: {who} ({codings})") # Contact: Jane (EMC) ``` ### PatientContactCategory Links one of the patient's contacts to one of the category codings the instance defines. Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime contact_person| PatientContactPerson category| ContactCategory ### ContactCategory A contact-category coding available in this Canvas instance — the set a contact's relationship can be drawn from. Use this to look up a coding before writing it with the [Patient effect](/sdk/effect-patient/#patientcontactcategory). Writing a coding that does not appear here is rejected rather than created, so querying this model first is how you find out what the instance actually has. Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime name| String code| String system| String protected| Boolean ```python from canvas_sdk.v1.data import ContactCategory from logger import log for coding in ContactCategory.objects.order_by("code"): log.info(f"{coding.code} / {coding.system} — {coding.name}") # EMC / INTERNAL — Emergency contact ``` ## Enumeration types ### SexAtBirth Value| Label ---|--- F| female M| male O| other UNK| unknown "" (empty string)| "" --- # PaymentCard Source: https://docs.canvasmedical.com/sdk/data-payment-card/ ## Introduction The `PaymentCard` model represents a patient's stored payment card on file. Cards are tokenized and stored by Canvas's payment processor, and this model exposes only the non-sensitive card metadata — brand, last four digits, expiration, cardholder name, and whether the card is the patient's default. The underlying processor token is never exposed through the SDK. Reference a `PaymentCard` by its Canvas `id` when charging a patient's card on file with the [Charge Stored Card](/sdk/effect-charge-stored-card/) effect. ## Basic usage Each `PaymentCard` is linked to a [Patient](/sdk/data-patient/#patient). Access a patient's stored cards through the `payment_cards` reverse relation: ```python >>> from canvas_sdk.v1.data import Patient >>> patient = Patient.objects.get(id="aebe4d3f5d18410388dc69c4b5169fc3") >>> cards = patient.payment_cards.all() >>> print([(card.brand, card.card_last_four_digits, card.is_default) for card in cards]) [('visa', '4242', True), ('mastercard', '5555', False)] ``` To retrieve the patient's default card: ```python >>> from canvas_sdk.v1.data import Patient >>> patient = Patient.objects.get(id="aebe4d3f5d18410388dc69c4b5169fc3") >>> default_card = patient.payment_cards.filter(is_default=True).first() ``` ## Attributes ### PaymentCard Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime patient| [Patient](/sdk/data-patient/#patient) brand| String card_last_four_digits| String expiration_month| String expiration_year| String card_holder_name| String postal_code| String is_default| Boolean --- # PayorSpecificCharge Source: https://docs.canvasmedical.com/sdk/data-payor-specific-charge/ ## Introduction The `PayorSpecificCharge` model represents charges specific to a [Transactor](/sdk/data-coverage/#transactor) in Canvas. ## Usage The `PayorSpecificCharge` model can be used to find all of the charges specific to a single `Transactor`: ```python >>> from canvas_sdk.v1.data import PayorSpecificCharge, Transactor >>> aetna = Transactor.objects.get(payer_id="60054") >>> aetna_charges = PayorSpecificCharge.objects.filter(transactor=aetna) >>> print([charge.charge_amount for charge in aetna_charges]) [150.00, 40.00, 99.99] ``` You can also access a transactor's specific charges from the `Transactor` model: ```python >>> from canvas_sdk.v1.data import Transactor >>> aetna = Transactor.objects.get(payer_id="60054") >>> aetna_charges = aetna.specific_charges.all() >>> print([charge.charge_amount for charge in aetna_charges]) [150.00, 40.00, 99.99] ``` ` ## Attributes ### PayorSpecificCharge Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime transactor| [Transactor](/sdk/data-coverage/#transactor) charge| [ChargeDescriptionMaster](/sdk/data-charge-description-master) charge_amount| Decimal effective_date| Date end_date| Date part_of_capitated_set| Boolean --- # Plan Source: https://docs.canvasmedical.com/sdk/data-plan/ ## Introduction The `Plan` model represents a Plan (plan of care) recorded on a Note, and is always associated with a Note and a Patient. It is the anchor for the [Plan](/sdk/commands/#plan) command. ## Basic usage To get a plan by identifier, use the `get` method on the `Plan` model manager: ```python from canvas_sdk.v1.data.plan import Plan plan = Plan.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, or note object, the plans for a patient or note can be accessed with the `plans` attribute on a `Patient` or `Note` object: ```python from canvas_sdk.v1.data.patient import Patient from canvas_sdk.v1.data.note import Note patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") plans = patient.plans.all() note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") plans = note.plans.all() ``` ## Reading the narrative The plan text is exposed through the `narrative` property: ```python from canvas_sdk.v1.data.plan import Plan plan = Plan.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") text = plan.narrative ``` ## Filtering Plans can be filtered by any attribute that exists on the model. ### Committed plans The `committed` method returns plans that have been committed and not entered in error: ```python from canvas_sdk.v1.data.plan import Plan committed_plans = Plan.objects.committed() ``` ## Attributes ### Plan Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) assessment| [Assessment](/sdk/data-assessment/#assessment) narrative| String --- # PluginCommand Source: https://docs.canvasmedical.com/sdk/data-plugin-command/ ## Introduction The `PluginCommand` model exposes the custom commands a plugin registers in its [`CANVAS_MANIFEST.json`](/sdk/canvas_manifest/#commands). Use it to read back a registered command's `label` and `section` instead of reconstructing display text from its camelCase `command_key`. ## Basic usage To get a plugin command by identifier, use the `get` method on the `PluginCommand` model manager: ```python from canvas_sdk.v1.data.plugin_command import PluginCommand plugin_command = PluginCommand.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` ## Filtering Plugin commands can be filtered by any attribute that exists on the model. Filtering for plugin commands is done with the `filter` method on the `PluginCommand` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.plugin_command import PluginCommand # Find all plugin commands in a specific chart section plugin_commands = PluginCommand.objects.filter(section="assessment") ``` ### By command key To find a registered command by the key declared in the manifest, filter on `command_key` — or on `schema_key`, which holds the same value: ```python from canvas_sdk.v1.data.plugin_command import PluginCommand plugin_command = PluginCommand.objects.filter(command_key="riskAssessment").first() if plugin_command: print(f"Label: {plugin_command.label}") print(f"Section: {plugin_command.section}") ``` ## Attributes ### PluginCommand Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime name| String command_key| String schema_key| String label| String section| String plugin_name| String - **id** : The unique UUID identifier for the plugin command. - **dbid** : The internal database primary key. - **name** : The registered name of the command (e.g., `RiskAssessment`). - **command_key** : The command key declared in the plugin's manifest (e.g., `riskAssessment`). There is exactly one row per `command_key`: reinstalling or upgrading the plugin updates that row in place, so a command always carries its current `label` and `section`. - **schema_key** : Always equal to `command_key`. It exists as its own field because chart command lines use the same name — see [`Command.schema_key`](/sdk/data-command/#command), which plugin authors query with `Command.objects.filter(schema_key="riskAssessment")`. Two installed plugins cannot declare the same key; the second install fails with a validation error. - **label** : The user-friendly display label for the command (e.g., `Risk Assessment`). - **section** : The chart section where the command appears: `subjective`, `objective`, `assessment`, `plan`, `procedures`, `history`, or `internal`. - **plugin_name** : The name of the plugin that registered the command. --- # Posting Source: https://docs.canvasmedical.com/sdk/data-posting/ ## Introduction This module defines models related to payments and postings associated with healthcare claims. ## Basic usage To retrieve a posting by ID: ```python from canvas_sdk.v1.data.posting import BasePosting posting = BasePosting.objects.get(dbid=1234) ``` To retrieve all active postings for a given claim: ```python from canvas_sdk.v1.data.claim import Claim claim = Claim.objects.get(id="") claim_postings = claim.postings.active() ``` Payments, adjustments, and transfers each record the claim line item the amount was posted to. To find the charge behind a payment, go through its claim line item: ```python from canvas_sdk.v1.data import NewLineItemPayment payment = NewLineItemPayment.objects.select_related("claim_line_item").get(dbid=1234) claim_line_item = payment.claim_line_item print(claim_line_item.proc_code) # None when the claim line item has no billing line item charge = claim_line_item.billing_line_item ``` > **Warning:** **Deprecation Notice:** The `billing_line_item` attribute on `NewLineItemPayment`, `NewLineItemAdjustment`, and `LineItemTransfer` is deprecated. It can return an unrelated charge, because the stored ID belongs to a claim line item, not a billing line item. Use `claim_line_item.billing_line_item` instead. `billing_line_item_id` holds the same claim line item ID as `claim_line_item_id`. ## Attributes ### BasePosting Base model for aggregating multiple line item-level transactions (payments, adjustments, transfers) associated with a claim. Field Name| Type| Description ---|---|--- dbid| Integer| corrected_posting| BasePosting| claim| [Claim](/sdk/data-claim/#claim)| payment_collection| PaymentCollection| description| String| entered_in_error| [CanvasUser](/sdk/data-canvasuser/)| created| DateTime| modified| DateTime| correction_postings| QuerySet[BasePosting]| paid_amount| Decimal (computed)| Total paid contractual_adjusted_amount| Decimal (computed)| Adjustments marked as write-offs non_write_off_adjusted_amount| Decimal (computed)| Non-write-off adjustments transferred_amount| Decimal (computed)| Total transferred transferred_to_patient_amount| Decimal (computed)| Portion transferred to the patient transferred_to_coverage_amount| Decimal (computed)| Portion transferred to another coverage adjusted_and_transferred_amount| Decimal (computed)| Combined adjusted and transferred amount posted_amount| Decimal (computed)| Total of payments and write-offs ### CoveragePosting Represents an insurance payment or adjustment associated with a claim's coverage. Field Name| Type ---|--- remittance| BaseRemittanceAdvice claim_coverage| [ClaimCoverage](/sdk/data-claim/#claimcoverage) crossover_carrier| String crossover_id| String payer_icn| String position_in_era| Integer ### PatientPosting Represents patient-side payments or adjustments, including links to copays or patient-level discounts. Field Name| Type| Description ---|---|--- claim_patient| [ClaimPatient](/sdk/data-claim/#claimpatient)| patient_payment| BulkPatientPosting| copay| BulkPatientPosting| discounted_amount| Decimal (computed)| Discount applied charges_amount| Decimal (computed)| Discount plus paid amount ### BulkPatientPosting Aggregates bulk patient payments on multiple claims. Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| payment_collection| PaymentCollection| total_paid| Decimal| created| DateTime| modified| DateTime| discount| Discount| payer| [Patient](/sdk/data-patient/)| postings| QuerySet[PatientPosting]| copays| QuerySet[PatientPosting]| total_posted_amount| Decimal (computed)| Sum of all posted amounts discounted_amount| Decimal (computed)| Sum of discounted amounts ### BaseRemittanceAdvice Represents shared data for both electronic and manual remittance advice. Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| payment_collection| PaymentCollection| total_paid| Decimal| created| DateTime| modified| DateTime| transactor| [Transactor](/sdk/data-coverage/#transactor)| era_id| String| postings| QuerySet[CoveragePosting]| total_posted_amount| Decimal (computed)| Sum of all posted amounts ### PaymentCollection Captures metadata about the method and details of a collected payment. Field Name| Type ---|--- id| UUID dbid| Integer total_collected| Decimal method| PostingMethods check_number| String check_date| Date deposit_date| Date description| String created| DateTime modified| DateTime postings| QuerySet[BasePosting] receipt| [Receipt](/sdk/data-receipt/#receipt) ### NewLineItemPayment Represents a payment applied to a claim line item. Field Name| Type ---|--- dbid| Integer posting| BasePosting billing_line_item| [BillingLineItem](/sdk/data-billing-line-item/) (deprecated) claim_line_item| [ClaimLineItem](/sdk/data-claim/#claimlineitem) amount| Decimal charged| Decimal entered_in_error| [CanvasUser](/sdk/data-canvasuser/) created| DateTime modified| DateTime ### NewLineItemAdjustment Represents an adjustment applied to a claim line item. Field Name| Type ---|--- dbid| Integer posting| BasePosting billing_line_item| [BillingLineItem](/sdk/data-billing-line-item/) (deprecated) claim_line_item| [ClaimLineItem](/sdk/data-claim/#claimlineitem) amount| Decimal code| String group| String deviated_from_posting_ruleset| Boolean write_off| Boolean entered_in_error| [CanvasUser](/sdk/data-canvasuser/) created| DateTime modified| DateTime ### LineItemTransfer Represents a transfer of a claim line item balance to another coverage or patient. Field Name| Type ---|--- dbid| Integer posting| BasePosting billing_line_item| [BillingLineItem](/sdk/data-billing-line-item/) (deprecated) claim_line_item| [ClaimLineItem](/sdk/data-claim/#claimlineitem) amount| Decimal code| String group| String deviated_from_posting_ruleset| Boolean transfer_to| [ClaimCoverage](/sdk/data-claim/#claimcoverage) transfer_to_patient| Boolean entered_in_error| [CanvasUser](/sdk/data-canvasuser/) created| DateTime modified| DateTime ### Discount Represents a discount applied to a claim or patient posting, linked by adjustment group and code. Field Name| Type ---|--- dbid| Integer name| String adjustment_group| String adjustment_code| String discount| Decimal created| DateTime modified| DateTime patient_postings| QuerySet[BulkPatientPosting] ## Enumeration types ### PostingMethods Value| Label ---|--- cash| Cash check| Check card| Card other| Other --- # Practice Location Source: https://docs.canvasmedical.com/sdk/data-practicelocation/ ## Introduction The `PracticeLocation` model lists all the clinical practice locations that fall under an [Organization](/sdk/data-organization). ## Basic usage To query a `PracticeLocation` by name, the `filter` method can be used like so: ```python from canvas_sdk.v1.data.practicelocation import PracticeLocation practice_location = PracticeLocation.objects.filter(full_name__icontains="downtown") ``` To retrieve a list of all practice locations: ```python from canvas_sdk.v1.data.practicelocation import PracticeLocation practice_locations = PracticeLocation.objects.all() ``` To query addresses that are associated with a `PracticeLocation`, related `PracticeLocationAddress` model instances can be accessed by using the `addresses` attribute. For exmample: ```python from canvas_sdk.v1.data.practicelocation import PracticeLocation practice_location = PracticeLocation.objects.first() practice_location_addresses = practice_location.addresses.all() ``` Each `PracticeLocation` has location-specific settings that control certain behavior within the EMR application. To retrieve the available settings for a `PracticeLocation` instance, the `settings` attribute can be used to retrieve a list of names: ```python from canvas_sdk.v1.data.practicelocation import PracticeLocation practice_location = PracticeLocation.objects.first() available_settings = practice_location.settings.values_list('name', flat=True) ``` Additionally, a setting's value can be found by accessing the `value` attribute on the `PracticeLocationSetting`: ```python from canvas_sdk.v1.data.practicelocation import PracticeLocation practice_location = PracticeLocation.objects.first() preferred_lab_partner_text = practice_location.settings.get(name="preferredLabPartner").value ``` Please note that the content of each `value` field can contain any value that is JSON-serializable, which includes string values. This means that `value` could be any of the Python types `string`, `list` or `dict`. ## Attributes ### PracticeLocation Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime organization| [Organization](/sdk/data-organization/#organization) place_of_service_code| String full_name| String short_name| String background_image_url| String background_gradient| String active| Boolean npi_number| String bill_through_organization| Boolean tax_id| String tax_id_type| [TaxIDType](/sdk/data-enumeration-types/#taxidtype) billing_location_name| String group_npi_number| String taxonomy_number| String include_zz_qualifier| Boolean addresses| PracticeLocationAddress settings| PracticeLocationSetting telecom| PracticeLocationContactPoint ### PracticeLocationAddress Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime practice_location| PracticeLocation line1| String line2| String city| String district| String state_code| String postal_code| String use| [AddressUse](/sdk/data-enumeration-types/#addressuse) type| [AddressType](/sdk/data-enumeration-types/#addresstype) longitude| Float latitude| Float start| Date end| Date country| String state| [AddressState](/sdk/data-enumeration-types/#addressstate) ### PracticeLocationSetting Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime practice_location| PracticeLocation name| String value| JSON ## PracticeLocationContactPoint The `PracticeLocationContactPoint` model represents a contact method (such as phone, email, or fax) for a Practice Location. Multiple contact points can be associated with a single Practice Location, each with its own type, use, and status. ### Attributes Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime practice_location| PracticeLocation system| [ContactPointSystem](/sdk/data-enumeration-types/#contactpointsystem) value| String use| [ContactPointUse](/sdk/data-enumeration-types/#contactpointuse) use_notes| String rank| Integer state| [ContactPointState](/sdk/data-enumeration-types/#contactpointstate) --- # PrescriptionChangeRequest Source: https://docs.canvasmedical.com/sdk/data-prescription-change-request/ ## Introduction The `PrescriptionChangeRequest` model represents an incoming Surescripts (NCPDP SCRIPT) request to change a prescription — for example, a generic substitution, a prior-authorization requirement, or a script clarification. Each request carries the raw request payload in its `content` attribute, the medication codings that describe the drug in question (`PrescriptionChangeRequestCoding`), and a reference to the original prescription it relates to. Because a `PrescriptionChangeRequest` originates from the pharmacy, its `patient`, `note`, and `staff` associations are nullable and may be unset. The provider's approve/deny decision is recorded as a [PrescriptionChangeResponse](/sdk/data-prescription-change-response/), which links back to the request and is available through the request's `response` reverse relation. ## Basic usage To get a prescription change request by identifier, use the `get` method on the `PrescriptionChangeRequest` model manager: ```python from canvas_sdk.v1.data import PrescriptionChangeRequest change_request = PrescriptionChangeRequest.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") ``` ## Related data A change request's medication codings are available through the `codings` reverse relation, and the responses recorded against it are available through the `response` reverse relation: ```python from canvas_sdk.v1.data import PrescriptionChangeRequest from logger import log change_request = PrescriptionChangeRequest.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") for coding in change_request.codings.all(): log.info(f"system: {coding.system}") log.info(f"code: {coding.code}") log.info(f"display: {coding.display}") responses = change_request.response.all() ``` The `PrescriptionChangeRequestCoding` entries represent the coding of the medication in question (for example, FDB or RxNorm), with an unstructured fallback whose `display` carries the drug description text when no structured code is available. ## Message content The `message_id` and `content` attributes carry the details of the inbound eRx message. `message_id` is the eRx (NCPDP SCRIPT / Surescripts) message identifier of the inbound change request. `content` is a JSON field holding the parsed inbound NCPDP SCRIPT change-request payload. It is a free-form, unstructured representation whose exact shape can vary between messages, typically including the pharmacy, the prescriber as reported by the sender, and the dispensed medication details (drug description, NDC, quantity, and similar). `content` defaults to an empty object (`{}`), so it is safe to call `.get()` on, but individual keys may be absent — plugins should access it defensively. ## Change types The `type_code` attribute identifies the kind of change the pharmacy is requesting: Code| Description ---|--- G| Generic Substitution P| Prior Authorization Required S| Therapeutic Interchange/Substitution D| Drug Use Evaluation S| Script Clarification OS| Pharmacy is out of stock U| Prescriber Authorization `S` really does carry two meanings. Canvas maps it to both Therapeutic Interchange/Substitution and Script Clarification, so the two are indistinguishable from `type_code` alone. The `sub_type_code` attribute further qualifies the request. It is nullable and currently supports: Code| Description ---|--- A| Confirm Prescriber State License ## Attributes ### PrescriptionChangeRequest Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) staff| [Staff](/sdk/data-staff/#staff) message_id| String original_prescription| [Prescription](/sdk/data-prescription/#prescription) type_code| PrescriptionChangeRequestType sub_type_code| PrescriptionChangeRequestSubType content| JSON codings| PrescriptionChangeRequestCoding[] response| [PrescriptionChangeResponse](/sdk/data-prescription-change-response/)[] ### PrescriptionChangeRequestCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime change_request| PrescriptionChangeRequest system| String version| String code| String display| String user_selected| Boolean ## Enumeration types ### PrescriptionChangeRequestType Name| Value| Label ---|---|--- GENERIC| G| Generic Substitution PRIOR| P| Prior Authorization Required SUBSTITUTION| S| Therapeutic Interchange/Substitution DRUG| D| Drug Use Evaluation OUTOFSTOCK| OS| Pharmacy is out of stock AUTHORIZATION| U| Prescriber Authorization ### PrescriptionChangeRequestSubType Name| Value| Label ---|---|--- LICENSE| A| Confirm Prescriber State License --- # PrescriptionChangeResponse Source: https://docs.canvasmedical.com/sdk/data-prescription-change-response/ ## Introduction The `PrescriptionChangeResponse` model is the anchor for the ApproveChange and DenyChange commands — a response to a Surescripts prescription change request, recorded on a Note. ## Basic usage To get a prescription change response by identifier, use the `get` method on the `PrescriptionChangeResponse` model manager: ```python from canvas_sdk.v1.data import PrescriptionChangeResponse response = PrescriptionChangeResponse.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the prescription change responses for a patient can be accessed with the `prescription_change_responses` attribute: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") responses = patient.prescription_change_responses.all() ``` The same attribute is available on a medication: ```python from canvas_sdk.v1.data import Medication medication = Medication.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") responses = medication.prescription_change_responses.all() ``` ## Committed records The `committed` method returns responses that have been committed and not entered in error: ```python from canvas_sdk.v1.data import PrescriptionChangeResponse committed_responses = PrescriptionChangeResponse.objects.committed() ``` ## Attributes ### PrescriptionChangeResponse Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) medication| [Medication](/sdk/data-medication) response_type| PrescriptionChangeResponseType status| PrescriptionChangeResponseStatus denied_medication| String refills| Integer note_to_pharmacist| String approved_drug_index| Integer reason_code| String message_id| String prior_authorization_number| String request| [PrescriptionChangeRequest](/sdk/data-prescription-change-request/) ## Enumeration types ### PrescriptionChangeResponseType Name| Value ---|--- APPROVED| A DENIED| D ### PrescriptionChangeResponseStatus Name| Value ---|--- OPEN| open PENDING| pending ULTIMATELY_ACCEPTED| ultimately-accepted ERROR| error --- # Prescription Source: https://docs.canvasmedical.com/sdk/data-prescription/ ## Introduction The `Prescription` model represents a prescription for a medication that has been written for a patient. Prescriptions track the full lifecycle of a medication order, including dosage details, pharmacy information, and electronic prescribing status. ## Basic usage To get a prescription by identifier, use the `get` method on the `Prescription` model manager: ```python from canvas_sdk.v1.data.prescription import Prescription prescription = Prescription.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the prescriptions for a patient can be accessed with the `prescriptions` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") prescriptions = patient.prescriptions.all() ``` If you have a patient ID, you can get the prescriptions for the patient with the `for_patient` method on the `Prescription` model manager: ```python from canvas_sdk.v1.data.prescription import Prescription patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" prescriptions = Prescription.objects.for_patient(patient_id) ``` ## Filtering Prescriptions can be filtered by any attribute that exists on the model. Filtering for prescriptions is done with the `filter` method on the `Prescription` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.prescription import Prescription, PrescriptionStatus prescriptions = Prescription.objects.filter(status=PrescriptionStatus.OPEN) ``` ```python from canvas_sdk.v1.data.prescription import Prescription, PrescriptionResponse approved_prescriptions = Prescription.objects.filter(response_type=PrescriptionResponse.APPROVED) ``` ### Active prescriptions The `active` method returns committed prescriptions that have not been denied: ```python from canvas_sdk.v1.data.prescription import Prescription active_prescriptions = Prescription.objects.active() ``` ### Committed prescriptions The `committed` method returns prescriptions that have been committed and not entered in error: ```python from canvas_sdk.v1.data.prescription import Prescription committed_prescriptions = Prescription.objects.committed() ``` ## Combined sig The `combined_sig` property returns a prescription's sig — its directions for use — as a string. It returns `sig_original_input`, and when `maximum_daily_dose` is set, `combined_sig` appends `. Maximum Daily Dose: {maximum_daily_dose}` to the sig. For example, a prescription with the sig `1 tab bid` and a maximum daily dose of `2 tabs` returns `1 tab bid. Maximum Daily Dose: 2 tabs`. When `maximum_daily_dose` is not set, `combined_sig` returns `sig_original_input` unchanged. When the prescription has no sig, `combined_sig` returns an empty string. Because `combined_sig` is a property computed in Python, it is read from each `Prescription` object and is not available to a queryset `.filter()` or `.order_by()`. ```python from canvas_sdk.v1.data.prescription import Prescription prescription = Prescription.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") sig = prescription.combined_sig # "" when the prescription has no sig ``` An active prescription's `combined_sig` is one of the sources a medication's [`latest_sig`](/sdk/data-medication/#latest-sig) draws on, alongside its change medications and medication statements. To get the most recent sig for the medication, read `latest_sig` from the referenced medication: ```python from canvas_sdk.v1.data.prescription import Prescription prescription = Prescription.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") sig = prescription.medication.latest_sig ``` ## Attributes ### Prescription Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| patient| [Patient](/sdk/data-patient/)| note| [Note](/sdk/data-note/)| assessment| [Assessment](/sdk/data-assessment/#assessment)| prescriber| [Staff](/sdk/data-staff/)| supervising_provider| [Staff](/sdk/data-staff/)| medication| [Medication](/sdk/data-medication/)| compound_medication| [CompoundMedication](/sdk/data-compound-medication/)| previous_medication| [Medication](/sdk/data-medication/)| indications| [Assessment](/sdk/data-assessment/)[]| related_refill| Prescription| refill_request| [RefillRequest](/sdk/data-refill-request/)| status| PrescriptionStatus| response_type| PrescriptionResponse| is_refill| Boolean| is_adjustment| Boolean| is_epcs| Boolean| generic_substitutions_allowed| Boolean| written_date| DateTime| dispensed_date| DateTime| end_date| Date| end_date_original_input| String| sig_original_input| String| dose_form| String| dose_route| String| dose_quantity| Float| dose_frequency| Float| dose_frequency_interval| String| maximum_daily_dose| String| combined_sig| String (computed)| See Combined sig potency_quantity| Float| dispense_quantity| Float| duration_in_days| Integer| count_of_refills_allowed| Integer| note_to_pharmacist| String| pharmacy_name| String| pharmacy_ncpdp_id| String| pharmacy_address| String| pharmacy_phone_number| String| pharmacy_fax_number| String| pharmacy_is_read_only| Boolean| message_id| String| prescription_order_number| String| reason_code| String| error_message| String| entered_in_error| [CanvasUser](/sdk/data-canvasuser)| committer| [CanvasUser](/sdk/data-canvasuser)| originator| [CanvasUser](/sdk/data-canvasuser)| created| DateTime| modified| DateTime| cancel_prescriptions| [CancelPrescription](/sdk/data-cancel-prescription/#cancelprescription)[]| change_requests| [PrescriptionChangeRequest](/sdk/data-prescription-change-request/#prescriptionchangerequest)[]| ## Enumeration types ### PrescriptionStatus Enum| Value| Label ---|---|--- OPEN| open| Open PENDING| pending| Pending ACCEPTED| ultimately-accepted| Ultimately Accepted ERROR| error| Error CANCEL_REQUESTED| cancel-requested| Cancel Requested CANCELED| canceled| Canceled CANCEL_DENIED| cancel-denied| Cancel Denied RECEIVED| received| Received by DrFirst SIGNED| signed| Signed INQUEUE| inqueue| In Queue TRANSMITTED| transmitted| Transmitted DELIVERED| delivered| Delivered ### PrescriptionResponse Enum| Value| Label ---|---|--- APPROVED| A| Approved APPROVED_WITH_CHANGES| C| Approved with changes DENIED| D| Denied DENIED_PRESCRIPTION_TO_FOLLOW| N| Denied, new prescription to follow --- # Procedure Source: https://docs.canvasmedical.com/sdk/data-procedure/ ## Introduction The `Procedure` model represents a procedure performed on or ordered for a patient. It is the data model behind the Perform command, is always associated with a Note and a Patient, and has an optional performing provider. Its CPT (or other) codings are available via `codings`. ## Basic usage To get a procedure by identifier, use the `get` method on the `Procedure` model manager: ```python from canvas_sdk.v1.data.procedure import Procedure procedure = Procedure.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient or note object, the procedures for a patient or note can be accessed with the `procedures` attribute: ```python from canvas_sdk.v1.data.patient import Patient from canvas_sdk.v1.data.note import Note patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") procedures = patient.procedures.all() note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") procedures = note.procedures.all() ``` If you have a patient ID, you can get the procedures for the patient with the `for_patient` method on the `Procedure` model manager: ```python from canvas_sdk.v1.data.procedure import Procedure patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" procedures = Procedure.objects.for_patient(patient_id) ``` ## Codings The codings for a procedure can be accessed with the `codings` attribute on a `Procedure` object: ```python from canvas_sdk.v1.data.procedure import Procedure from logger import log procedure = Procedure.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") for coding in procedure.codings.all(): log.info(f"system: {coding.system}") log.info(f"code: {coding.code}") log.info(f"display: {coding.display}") ``` ## Filtering Procedures can be filtered by any attribute that exists on the model. Filtering for procedures is done with the `filter` method on the `Procedure` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.procedure import Procedure, ProcedureStatus procedures = Procedure.objects.filter(status=ProcedureStatus.COMPLETED) ``` ### Committed procedures The `committed` method returns procedures that have been committed and not entered in error: ```python from canvas_sdk.v1.data.procedure import Procedure committed_procedures = Procedure.objects.committed() ``` ### By ValueSet See [Value Sets](/sdk/data-value-sets/) for the library of built-in value sets and how to create your own. Filtering by ValueSet works a little differently. The `find` method on the model manager is used to perform `ValueSet` filtering: ```python from canvas_sdk.v1.data.procedure import Procedure from canvas_sdk.value_set.v2022.procedure import Colonoscopy procedures = Procedure.objects.find(Colonoscopy) ``` ## Attributes ### Procedure Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) assessment| [Assessment](/sdk/data-assessment/#assessment) provider| [Staff](/sdk/data-staff/) status| ProcedureStatus notes| String codings| ProcedureCoding[] ### ProcedureCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime system| String version| String code| String display| String user_selected| Boolean procedure| Procedure ## Enumeration types ### ProcedureStatus Name| Value| Label ---|---|--- IN_PROGRESS| 1| in-progress ABORTED| 2| aborted COMPLETED| 3| completed --- # Protocol Current Source: https://docs.canvasmedical.com/sdk/data-protocol-current/ ## Introduction The `ProtocolCurrent` object represents the current state of clinical protocols applied to patients within Canvas. Protocols are typically structured plans or guidelines that outline specific medical interventions, treatments, or care pathways for managing various health conditions. The `ProtocolCurrent` object contains essential information about the protocol's status, associated patient, and relevant clinical details. ## Basic Usage To get a protocol by identifier, use the `get` method on the `ProtocolCurrent` model manager: ```python from canvas_sdk.v1.data.protocol_current import ProtocolCurrent protocol = ProtocolCurrent.objects.get(id="12345678-1234-1234-1234-123456789012") ``` ## Filtering ```python from canvas_sdk.v1.data.protocol_current import ProtocolCurrent protocols = ProtocolCurrent.objects.filter(status="active", patient_id="b80b1cdc2e6a4aca90ccebc02e683f35") ``` ## Attributes ### ProtocolResult Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime title| String narrative| String result_identifiers| Array[String] types| Array[String] protocol_key| String plugin_name| String status| String due_in| DateTime days_of_notice| Integer snoozed| Boolean sources| Array[String] recommendations| Array[String] top_recommendation_key| String next_review| DateTime feedback_enabled| Boolean plugin_can_be_snoozed| Boolean patient_id| UUID result_hash| String snooze_date| DateTime --- # ProtocolOverride Source: https://docs.canvasmedical.com/sdk/data-protocol-override/ ## Introduction The `ProtocolOverride` model represents an instance of a protocol being snoozed for a patient. ## Basic usage To get a protocol override by identifier, use the `get` method on the `ProtocolOverride` model manager: ```python from canvas_sdk.v1.data.protocol_override import ProtocolOverride protocol_override = ProtocolOverride.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the protocol overrides for a patient can be accessed with the `protocol_overrides` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") overrides = patient.protocol_overrides.all() ``` If you have a patient ID, you can get the protocol overrides for the patient with the `for_patient` method on the `ProtocolOverride` model manager: ```python from canvas_sdk.v1.data.protocol_override import ProtocolOverride patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" override = ProtocolOverride.objects.for_patient(patient_id) ``` ## Filtering Protocol overrides can be filtered by any attribute that exists on the model. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.protocol_override import ProtocolOverride overrides = ProtocolOverride.objects.filter(status="active") ``` ## Convenience methods The `ProtocolOverride` model manager includes convenience methods for the filters plugins most often apply when working with protocol overrides. `active` returns the overrides whose `status` is `active`: ```python from canvas_sdk.v1.data.protocol_override import ProtocolOverride active_overrides = ProtocolOverride.objects.active() ``` `adjustments` returns the adjustment overrides (`is_adjustment=True`) for a given protocol key, and `snoozes` returns the snooze overrides (`is_snooze=True`) for a given protocol key: ```python from canvas_sdk.v1.data.protocol_override import ProtocolOverride adjustments = ProtocolOverride.objects.adjustments("HCC001v1") snoozes = ProtocolOverride.objects.snoozes("HCC001v1") ``` Each method returns a queryset, so you can chain them with `for_patient`, `committed`, and with one another. For example, to get the active adjustments for a given patient and protocol key: ```python from canvas_sdk.v1.data.protocol_override import ProtocolOverride adjustments = ( ProtocolOverride.objects .for_patient("1eed3ea2a8d546a1b681a2a45de1d790") .committed() .active() .adjustments("HCC001v1") ) ``` ## Attributes ### ProtocolOverride Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) protocol_key| String is_adjustment| Boolean reference_date| DateTime cycle_in_days| Integer is_snooze| Boolean snooze_date| Date snoozed_days| Integer snooze_comment| String narrative| String cycle_quantity| Integer cycle_unit| IntervalUnit status| Status ## Enumeration types ### IntervalUnit Value| Label ---|--- days| days months| months years| years ### Status Value| Label ---|--- active| active inactive| inactive --- # Questionnaire Source: https://docs.canvasmedical.com/sdk/data-questionnaire/ ## Introduction The `Questionnaire` model represents a structured set of questions intended to guide the collection of answers from end-users. The `Interview` model represents answers to a structured set of questions represented by a `Questionnaire`. ## Basic usage To get a questionnaire or interview by identifier, use the `get` method on the `Questionnaire` or `Interview` model managers: ```python from canvas_sdk.v1.data.questionnaire import Interview, Questionnaire questionnaire = Questionnaire.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") interview = Interview.objects.get(id="75df6d7f-d58d-443b-9fa0-ce43b4d7b2a0") ``` If you have a patient object, the interviews for a patient can be accessed with the `interviews` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") interviews = patient.interviews.all() ``` If you have a patient ID, you can get the interviews for the patient with the `for_patient` method on the `Interview` model manager: ```python from canvas_sdk.v1.data.questionnaire import Interview patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" interviews = Interview.objects.for_patient(patient_id) ``` ## Questionnaire questions The questions for a questionnaire can be accessed with the `questions` attribute on an `Questionnaire` object: ```python from canvas_sdk.v1.data.questionnaire import Questionnaire from logger import log questionnaire = Questionnaire.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") for question in questionnaire.questions.all(): log.info(f"system: {question.code_system}") log.info(f"code: {question.code}") log.info(f"name: {question.name}") ``` ## Interview responses The interview responses for an interview can be accessed with the `interview_responses` attribute on an `Interview` object: ```python from canvas_sdk.v1.data.questionnaire import Interview from logger import log interview = Interview.objects.get(id="75df6d7f-d58d-443b-9fa0-ce43b4d7b2a0") for interview_response in interview.interview_responses.all(): log.info(f"response option: {interview_response.response_option_value}") ``` > **Info:** A date question writes its answer to `response_option_date` as a real date, and to `response_option_value` as the same date in ISO-8601 form. Use `response_option_date` to compare or to filter, because date lookups such as `__gte` and `__year` work only on it. It is `None` for every other question type, and also when nobody answered the date question. ## Filtering Questionnaires and interviews can be filtered by any attribute that exists on the models. Filtering for questionnaires and interviews is done with the `filter` method on the `Questionnaire` and `Interview` model managers. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.questionnaire import Interview, Questionnaire questionnaires = Questionnaire.objects.filter(name="Tobacco") interviews = Interview.objects.filter(progress_status="F") ``` ### By ValueSet See [Value Sets](/sdk/data-value-sets/) for the library of built-in value sets and how to create your own. Filtering by ValueSet works a little differently. The `find` method on the model manager is used to perform `ValueSet` filtering: ```python from canvas_sdk.v1.data.questionnaire import Questionnaire from canvas_sdk.value_set.v2022.assessment import TobaccoUseScreening questionnaires = Questionnaire.objects.find(TobaccoUseScreening) ``` `Interview` also supports `find`, which returns the interviews whose questionnaire has a code in the value set: ```python from canvas_sdk.v1.data.questionnaire import Interview from canvas_sdk.value_set.v2022.assessment import TobaccoUseScreening interviews = Interview.objects.find(TobaccoUseScreening) ``` For interviews, `find` matches against the related `Questionnaire` through the `questionnaires` relation. Questionnaires store their code system by name (for example, `"LOINC"`) rather than by URL, and `find` handles this for you. It also composes with `for_patient`: ```python from canvas_sdk.v1.data.questionnaire import Interview from canvas_sdk.value_set.v2022.assessment import TobaccoUseScreening interviews = ( Interview.objects .for_patient("1eed3ea2a8d546a1b681a2a45de1d790") .find(TobaccoUseScreening) ) ``` ## Attributes ### ResponseOptionSet Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime status| String name| String code_system| String code| String type| String — one of the question types below use_in_shx| Boolean options| ResponseOption[] questions| Question[] #### Question types `type` holds the code for the kind of question the option set describes. It decides how the question renders in a note and which value an answer carries. `type`| Question| Answer ---|---|--- `TXT`| Free text| Text, on the response's `response_option_value`. `INT`| Integer| A whole number. `DEC`| Decimal| A decimal number. `DATE`| Date| A calendar date, on the response's `response_option_date`. `SING`| Single select| One ResponseOption. `MULT`| Multi select| One or more ResponseOption records. `TXT` and `DATE` questions are not scored, so they are skipped when a questionnaire calculates a score. Authoring a questionnaire in a plugin sets this through the question's `responses_type` — see [Response types](/sdk/effect-questionnaires/#response-types). ### ResponseOption Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime status| String name| String code| String code_description| String value| String response_option_set| ResponseOptionSet ordering| Integer interview_responses| InterviewQuestionResponse[] enablement_conditions| QuestionEnablementCondition[] `enablement_conditions` holds the conditions that test for this response option, which is how you find the questions a given answer unlocks. ### Question Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime status| String name| String response_option_set| ResponseOptionSet acknowledge_only| Boolean show_prologue| Boolean code_system| String code| String enable_behavior| String — `all` or `any` interview_responses| InterviewQuestionResponse[] dependent_conditions| QuestionEnablementCondition[] triggers_condition| QuestionEnablementCondition[] `enable_behavior` decides whether all of the question's enablement conditions must be met for it to be enabled, or only one of them. It is empty on a question authored without an `enabled_behavior`, so handle an empty value when reading it. The read-side attribute here is `enable_behavior` (no "d"). When authoring a questionnaire through the effect or the manifest schema, the matching config field is spelled `enabled_behavior` (with a "d"). The difference is intentional. A question sits on both sides of the branching, so it carries a relation for each direction: - `dependent_conditions`: the conditions that decide whether this question is enabled. - `triggers_condition`: the conditions on other questions that test this question's answer. ### QuestionEnablementCondition A `QuestionEnablementCondition` controls when a `Question` is enabled, following FHIR's `enableWhen` pattern. Import it with `from canvas_sdk.v1.data import QuestionEnablementCondition`. Here `question` is the question the condition governs, and `dependent_on` is the question whose answer is tested. Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime status| String question| Question dependent_on| Question operator| String — `=`, `!=`, `exists`, or `not_exists` answer_option| ResponseOption answer_value| String `operator` takes the same comparison operators as a questionnaire's enabled conditions (see [Enablement operators](/sdk/effect-questionnaires/#enablement-operators)). `answer_option` references the ResponseOption matched, the read-side counterpart of a condition's `value_code`; `answer_value` holds a literal value matched, the counterpart of a condition's `value_string`. ### Questionnaire Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime status| String name| String expected_completion_time| Float can_originate_in_charting| Boolean use_case_in_charting| String scoring_function_name| String scoring_code_system| String scoring_code| String code_system| String code| String search_tags| String questions| Question[] use_in_shx| Boolean carry_forward| String interview_responses| InterviewQuestionResponse[] ### QuestionnaireQuestionMap Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime status| String questionnaire| Questionnaire question| Question ### Interview Field Name| Type ---|--- id| UUID dbid| Integer committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) status| String name| String language_id| Integer use_case_in_charting| String patient| [Patient](/sdk/data-patient/#patient) note_id| Integer appointment_id| Integer questionnaires| Questionnaire[] progress_status| String created| DateTime modified| DateTime interview_responses| InterviewQuestionResponse[] assessment_set| [Assessment](/sdk/data-assessment/#assessment)[] ### InterviewQuestionnaireMap The join between an Interview and the questionnaires it covers. Reach them through the interview's `questionnaires` attribute rather than querying this model. Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime status| String interview| Interview questionnaire| Questionnaire ### InterviewQuestionResponse Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime status| String interview| Interview questionnaire| Questionnaire question| Question response_option| ResponseOption response_option_value| String response_option_date| Date questionnaire_state| String interview_state| String comment| String --- # ReasonForVisit Source: https://docs.canvasmedical.com/sdk/data-reason-for-visit/ ## Introduction This page covers three models: - `ReasonForVisit` — a Reason for Visit recorded on a note, and the anchor for the [Reason for Visit](/sdk/commands/#reasonforvisit) command. - `ReasonForVisitCoding` — the codings on a recorded Reason for Visit. - `ReasonForVisitSettingCoding` — the configured codings an instance offers, used to populate the coding field when a Reason for Visit is recorded. ## ReasonForVisit A `ReasonForVisit` is always associated with a note and a patient. To get one by identifier: ```python from canvas_sdk.v1.data import ReasonForVisit rfv = ReasonForVisit.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` From a patient or a note, use the `reasons_for_visit` attribute: ```python from canvas_sdk.v1.data import Note, Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") reasons = patient.reasons_for_visit.all() note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") reasons = note.reasons_for_visit.all() ``` ### Reading the narrative The text is exposed through the `narrative` property, which returns the free-text value when there is one and otherwise renders the structured `narrative_json`: ```python from canvas_sdk.v1.data import ReasonForVisit rfv = ReasonForVisit.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") text = rfv.narrative ``` ### Commit state The Reason for Visit command has no commit step, so a recorded reason is readable as soon as it exists. Read the records directly, as shown above, rather than filtering for committed ones. > **Warning:** `committed()` is inherited from the shared queryset for committable models, so it runs against `ReasonForVisit` without raising. It filters on `committer`, a field the Reason for Visit command has no step to populate, so it can return nothing at all. For the actions this command does support, see the command types table under [Commands](/sdk/effects/#commands) in Effects. ### Codings Each `ReasonForVisit` exposes its codings through `codings`: ```python from canvas_sdk.v1.data import ReasonForVisit rfv = ReasonForVisit.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") codings = rfv.codings.all() ``` ### Attributes #### ReasonForVisit Field Name| Type| Description ---|---|--- id| UUID| The universally unique identifier for this record. dbid| Integer| The database identifier for this record. created| DateTime| When the record was created. modified| DateTime| When the record was last modified. originator| [CanvasUser](/sdk/data-canvasuser)| The user who originated the command. committer| [CanvasUser](/sdk/data-canvasuser)| The user who committed the record, if set. The Reason for Visit command has no commit step, so this field is not reliably populated. entered_in_error| [CanvasUser](/sdk/data-canvasuser)| The user who entered the record in error, if it has been. patient| [Patient](/sdk/data-patient/#patient)| The patient the reason for visit was recorded for. note| [Note](/sdk/data-note)| The note it was recorded on. narrative| String| The reason for visit text. codings| _list_| The `ReasonForVisitCoding` records on this reason for visit. #### ReasonForVisitCoding Field Name| Type| Description ---|---|--- dbid| Integer| The database identifier for this coding record. created| DateTime| When the record was created. modified| DateTime| When the record was last modified. code| String| The code representing the concept. display| String| The human-readable display name for the concept. system| String| The coding system. version| String| The version of the coding system. user_selected| Boolean| Whether a user chose this coding directly. reason_for_visit| ReasonForVisit| The reason for visit this coding belongs to. ## ReasonForVisitSettingCoding The `ReasonForVisitSettingCoding` model represents the coding information used to populate the coding field within a Reason For Visit in Canvas. ### Basic Usage To retrieve a specific coding record by its identifier, use the model manager's `get` method: ```python from canvas_sdk.v1.data import ReasonForVisitSettingCoding rfv_coding = ReasonForVisitSettingCoding.objects.get(id="e2b1e1e3-3f52-4a0a-bb3a-123456789abc") ``` You can also filter records by attributes. For example, to get all codings from a specific coding system: ```python from canvas_sdk.v1.data import ReasonForVisitSettingCoding codings = ReasonForVisitSettingCoding.objects.filter(system="http://snomed.info/sct") ``` ### Attributes #### ReasonForVisitSettingCoding Field Name| Type| Description ---|---|--- id| UUID| The universally unique identifier for this coding record. dbid| Integer| The database identifier for this coding record. created| DateTime| When the record was created. modified| DateTime| When the record was last modified. code| String| The code representing the concept. display| String| The human-readable display name for the concept. system| String| The coding system (e.g., `http://snomed.info/sct`). version| String| The version of the coding system. duration| Array of Duration| An array of durations (as Python `timedelta` objects) associated with the coding. user_selected| Boolean| The active/inactive flag for this reason-for-visit coding: `True` = active, `False` = inactive. --- # Receipt Source: https://docs.canvasmedical.com/sdk/data-receipt/ The `Receipt` model represents the single canonical payment receipt Canvas generates for a payment collection. Canvas creates one receipt per [PaymentCollection](/sdk/data-posting/#paymentcollection), and the model exposes that receipt's PDF through a presigned URL. ## Basic Usage ```python from canvas_sdk.v1.data import Receipt receipt = Receipt.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") ``` Each receipt belongs to one [PaymentCollection](/sdk/data-posting/#paymentcollection), and you can reach it from the collection through the one-to-one reverse accessor: ```python from canvas_sdk.v1.data import PaymentCollection collection = PaymentCollection.objects.get(id="f47ac10b-58cc-4372-a567-0e02b2c3d479") receipt = collection.receipt # the one-to-one Receipt; raises if the collection has none ``` ## Finding a patient's receipts A patient-portal plugin should show only the signed-in patient's receipts. A receipt belongs to a [PaymentCollection](/sdk/data-posting/#paymentcollection), and the patient who paid is the `payer` on that collection's [BulkPatientPosting](/sdk/data-posting/#bulkpatientposting), so filter on that path: ```python from canvas_sdk.v1.data import Patient, Receipt patient = Patient.objects.get(id="b80b1cdc2e6a4aca90ccebc02e683f35") receipts = Receipt.objects.filter( payment_collection__bulkpatientposting__payer=patient, entered_in_error__isnull=True, ) ``` ## Accessing the receipt PDF The `receipt_url` property returns a presigned S3 URL for the receipt PDF. The URL is valid for one hour and is regenerated on each access, so fetch it at render time rather than persisting or caching it. It returns `None` when the receipt has no PDF. The `receipt` field itself holds the stored file path, not a URL, so use `receipt_url` to link a patient to their PDF. ```python from canvas_sdk.v1.data import Receipt receipt = Receipt.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") url = receipt.receipt_url # presigned S3 URL (valid for 1 hour), or None ``` ## Attributes ### Receipt Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| originator| [CanvasUser](/sdk/data-canvasuser/)| entered_in_error| [CanvasUser](/sdk/data-canvasuser/)| payment_collection| [PaymentCollection](/sdk/data-posting/#paymentcollection)| account_balance_before_collection| Decimal| account_balance_after_collection| Decimal| discount| Decimal| receipt| String| receipt_url| String (computed)| Presigned S3 URL for the receipt PDF, valid for 1 hour, or `None` while the PDF is still being generated total_posted_amount| Decimal (computed)| Sum of the payments and write-off adjustments across the payment collection's active postings copay_amount| Decimal (computed)| Amount posted as copays with the payment collection - `account_balance_before_collection` and `account_balance_after_collection` are snapshots of the patient's account balance taken when the receipt was generated. They don't change as the balance changes. - `discount` is the discount amount recorded on the receipt, or `0.00` when no discount was applied. --- # Reference Source: https://docs.canvasmedical.com/sdk/data-reference/ ## Introduction The `Reference` model is the record of a diagnostic view embedded in a note, created when the [Reference](/sdk/commands/#reference) command is originated, for example when a clinician adds a diagnostic view to a note through diagnostic theater, or a plugin originates a `ReferenceCommand`. It captures a snapshot of the referenced diagnostic view's rendered table. The command creates the reference record when it is originated, and commits it when the command is committed. ## Basic usage To get a reference by identifier, use the `get` method on the `Reference` model manager: ```python from canvas_sdk.v1.data import Reference reference = Reference.objects.get(id="a1b2c3d4-e5f6-7890-abcd-ef1234567890") ``` If you have a patient object, the references for a patient can be accessed with the `references` attribute on a `Patient` object: ```python from canvas_sdk.v1.data import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") references = patient.references.all() ``` If you have a note object, the references on that note can be accessed with the `references` attribute on a `Note` object: ```python from canvas_sdk.v1.data import Note note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") references = note.references.all() ``` ## Committed references A reference stays staged until its command is committed, so it can exist in an uncommitted state while the command is staged. The basic-usage queries above return staged and committed references together. Use the `committed` method to get only references that have been committed and not entered in error: ```python from canvas_sdk.v1.data import Reference committed = Reference.objects.committed() ``` ## Attributes ### Reference Field Name| Type| Description ---|---|--- id| UUID| The universally unique identifier for this record. dbid| Integer| The database identifier for this record. created| DateTime| When the record was created. modified| DateTime| When the record was last modified. originator| [CanvasUser](/sdk/data-canvasuser)| The user who originated the command. committer| [CanvasUser](/sdk/data-canvasuser)| The user who committed the command, if it has been committed. entered_in_error| [CanvasUser](/sdk/data-canvasuser)| The user who entered the record in error, if it has been. patient| [Patient](/sdk/data-patient/#patient)| The patient the reference was recorded for. note| [Note](/sdk/data-note/#note)| The note it was recorded on. diagnostic_view| [DiagnosticView](/sdk/data-diagnostic-view/#diagnosticview)| The diagnostic view that was referenced. May be null if no view was set when the command was originated, or if the supplied view id did not match a diagnostic view on the instance and was discarded. name| String (up to 100 characters)| The label shown on the note, captured at origination. May be an empty string. Derived from the diagnostic view when the command is originated and not recalculated if the command is later pointed at a different diagnostic view with `edit()`. content| JSON| A snapshot of the referenced diagnostic view's rendered table, captured when the reference was created and not updated if the command is later pointed at a different diagnostic view with `edit()`. --- # Referral Source: https://docs.canvasmedical.com/sdk/data-referral/ ## Introduction The `Referral`, `ReferralReport`, and `ReferralReview` models represent referral results and their reviews. ## Basic Usage To retrieve a `Referral`, `ReferralReport`, or `ReferralReview` by identifier, use the `get` method on the model manager: ```python from canvas_sdk.v1.data.referral import Referral, ReferralReport, ReferralReview referral = Referral.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") referral_report = ReferralReport.objects.get(id="c1a5a35a-4ee2-4a0e-85c0-21739dc8c4a8") referral_review = ReferralReview.objects.get(id="b3e6f74c-2a1b-4c8d-9f2e-31842ae7d3b9") ``` If you have a patient object, the referrals, reports, and reviews can be accessed with the `referral_set`, `referral_reports`, and `referral_reviews` attributes, respectively on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") referrals = patient.referral_set.all() reports = patient.referral_reports.all() reviews = patient.referral_reviews.all() ``` ## Filtering Referrals, reports, and reviews can be filtered by any attribute that exists on the models. Filtering is done with the `filter` method on the `Referral`, `ReferralReport`, and `ReferralReview` model managers. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.referral import Referral, ReferralReport, ReferralReview referrals = Referral.objects.filter(priority="urgent") reports = ReferralReport.objects.filter(requires_signature=True) reviews = ReferralReview.objects.filter(status="completed") ``` ### By ValueSet See [Value Sets](/sdk/data-value-sets/) for the library of built-in value sets and how to create your own. `ReferralReport` supports `ValueSet` filtering through the `find` method on its model manager: ```python from canvas_sdk.v1.data.referral import ReferralReport from canvas_sdk.value_set.v2022.procedure import DialysisServices reports = ReferralReport.objects.find(DialysisServices) ``` `find` joins through the report's `codings` reverse relation and matches on `(system, code)` pairs from the value set, so a coding must match both the code system and the code to be included. ### Committed records The `committed` method returns `Referral` and `ReferralReview` records that have been committed and not entered in error: ```python from canvas_sdk.v1.data.referral import Referral, ReferralReview committed_referrals = Referral.objects.committed() committed_reviews = ReferralReview.objects.committed() ``` ## Related Tasks To retrieve an Referral's related tasks, use the `get_task_objects` method on the Referral object. ```python from canvas_sdk.v1.data.referral import Referral referral = Referral.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") tasks = referral.get_task_objects().all() ``` The `task_list` computed property returns the same related tasks as a `list[Task]`: ```python from canvas_sdk.v1.data.referral import Referral referral = Referral.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") tasks = referral.task_list ``` ## Review assignment A referral report is assigned for review to staff members or to a team. `reviewers` holds the assigned staff members, and `team` holds the assigned team, or `None` when no team is assigned. ```python from canvas_sdk.v1.data import ReferralReport, Staff referral_report = ReferralReport.objects.get(id="c1a5a35a-4ee2-4a0e-85c0-21739dc8c4a8") # The staff members and the team the referral report is assigned to reviewers = referral_report.reviewers.all() team = referral_report.team # Every referral report assigned to this staff member staff = Staff.objects.get(id="4150cd20de8a470aa570a852859ac87e") assigned = ReferralReport.objects.filter(reviewers__id=staff.id) ``` ## The document reference `ReferralReport` carries the consult report's specialty, review state and comments, not the file. Canvas stores the file on a [DocumentReference](/sdk/data-document-reference/#the-related-object) pointing back at the report, which is also how it appears in the FHIR API. To read it, resolve the [ContentType](/sdk/data-content-type/) at runtime from its stable `app_label` and `model` — never hardcode the per-environment `dbid` — and match `object_id` against the report's `dbid`: ```python from canvas_sdk.v1.data import ContentType, DocumentReference, ReferralReport report = ReferralReport.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") content_type = ContentType.objects.filter(app_label="api", model="referralreport").first() document = DocumentReference.objects.filter( content_type=content_type, object_id=report.dbid ).first() url = document.document_url if document else None ``` > **Info:** `object_id` holds the related record's integer `dbid`, not its UUID `id`. ## Attributes ### Referral Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note/#note) assessment| [Assessment](/sdk/data-assessment/#assessment) assessments| [Assessment](/sdk/data-assessment/#assessment) service_provider| [ServiceProvider](/sdk/data-serviceprovider/#service-provider) clinical_question| String priority| String include_visit_note| Boolean notes| String date_referred| DateTime internal_comment| String forwarded| Boolean ignored| Boolean internal_task_comment| [TaskComment](/sdk/data-task/#taskcomment) task_ids| String reports| ReferralReport[] action_events| [ReferralActionEvent](/sdk/data-fax/#referralactionevent)[] ### ReferralReport Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) assigned_by| [CanvasUser](/sdk/data-canvasuser) review_mode| [DocumentReviewMode](/sdk/data-enumeration-types/#documentreviewmode) junked| Boolean requires_signature| Boolean assigned_date| DateTime team_assigned_date| DateTime team| [Team](/sdk/data-team/#team) reviewers| [Staff](/sdk/data-staff/#staff)[] patient| [Patient](/sdk/data-patient/#patient) referral| Referral specialty| String comment| String priority| Boolean original_date| Date review| ReferralReview codings| ReferralReportCoding[] ### ReferralReview Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) internal_comment| String message_to_patient| String status| String note| [Note](/sdk/data-note/#note) patient| [Patient](/sdk/data-patient/#patient) patient_communication_method| String reports| ReferralReport[] ### ReferralReportCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime report| ReferralReport system| String version| String code| String display| String user_selected| Boolean value| String --- # RefillRequest Source: https://docs.canvasmedical.com/sdk/data-refill-request/ ## Introduction The `RefillRequest` model represents an incoming request to refill a patient's medication — for example, a renewal request received electronically from a pharmacy. Each request carries the raw request payload in its `content` attribute, the associated patient and staff member, the medication codings that describe the requested drug (`RefillRequestCoding`), and the prescription(s) written in response. An incoming `RefillRequest` is routed to a [staff](/sdk/data-staff/#staff) member — the provider expected to respond to it, who becomes the `prescriber` of the responding prescription, not the original requester — and can be marked as `ignored` to drop it from the active refill worklist. Once acted on, the request links to the responding prescription(s) through its `response` attribute, and each [Prescription](/sdk/data-prescription/#prescription) points back to the request through its `refill_request` field. Because the request originates from the pharmacy, and a pharmacy may route it to a provider other than the original prescriber, `staff` is not the requester; it is also nullable, so it may be unset. ## Basic usage To get a refill request by identifier, use the `get` method on the `RefillRequest` model manager: ```python from canvas_sdk.v1.data.refill_request import RefillRequest refill_request = RefillRequest.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") ``` If you have a patient object, the refill requests for a patient can be accessed with the `refill_requests` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") refill_requests = patient.refill_requests.all() ``` If you have a patient ID, you can get the refill requests for the patient with the `for_patient` method on the `RefillRequest` model manager: ```python from canvas_sdk.v1.data.refill_request import RefillRequest patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" refill_requests = RefillRequest.objects.for_patient(patient_id) ``` ## Filtering Refill requests can be filtered by any attribute that exists on the model. Filtering is done with the `filter` method on the `RefillRequest` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.refill_request import RefillRequest outstanding_requests = RefillRequest.objects.filter(ignored=False) ``` The `ignored` attribute is a boolean dismiss flag (default `False`). Marking a request ignored removes it from the active refill worklist and is used to suppress duplicates — for example, a pharmacy re-sending a request. Requests that have already been responded to are excluded from the worklist separately, through their linked `response`. A plugin can filter on `ignored`, but setting it is not available through the data module. The example above returns the active (non-dismissed) requests. ## Related data A refill request's medication codings are available through the `codings` reverse relation, and the prescriptions written in response are available through the `response` reverse relation: ```python from canvas_sdk.v1.data.refill_request import RefillRequest from logger import log refill_request = RefillRequest.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") for coding in refill_request.codings.all(): log.info(f"system: {coding.system}") log.info(f"code: {coding.code}") log.info(f"display: {coding.display}") responding_prescriptions = refill_request.response.all() ``` The `RefillRequestCoding` entries represent the coding of the requested drug (for example, FDB or RxNorm), with an unstructured fallback whose `display` carries the drug description text when no structured code is available. ## Message content The `message_id` and `content` attributes carry the details of the inbound eRx message. `message_id` is the eRx (NCPDP SCRIPT / Surescripts) message identifier of the inbound refill-renewal request itself. `content` is a JSON field holding the parsed inbound NCPDP SCRIPT refill-renewal request payload. It is a free-form, unstructured representation whose exact shape can vary between messages. The information typically available includes: - the pharmacy (name, NCPDP ID, phone, address) - the prescriber as reported by the sender (name, NPI, SPI, and a sender-supplied identifier) - the dispensed and prescribed medication details (drug description, NDC, quantity, days supply, directions, number of refills, substitution allowance, written date, and similar) - reference identifiers such as the Rx reference number and the message ID of the original prescription it renews — distinct from this request's own `message_id` `content` is always a JSON object — it defaults to an empty object (`{}`) when no data was captured — so it is safe to call `.get()` on, but individual keys may be absent. Because the shape is not guaranteed, plugins should access `content` defensively, checking that a key is present before relying on it. ## Attributes ### RefillRequest Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime patient| [Patient](/sdk/data-patient/#patient) staff| [Staff](/sdk/data-staff/#staff) message_id| String ignored| Boolean content| JSON codings| RefillRequestCoding[] response| [Prescription](/sdk/data-prescription/#prescription)[] ### RefillRequestCoding Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime refill_request| RefillRequest system| String version| String code| String display| String user_selected| Boolean --- # Remove Allergy Event Source: https://docs.canvasmedical.com/sdk/data-remove-allergy-event/ ## Introduction The `RemoveAllergyEvent` model represents a record of an allergy being removed from a patient's allergy list — the anchor for the [Remove Allergy](/sdk/commands/#removeallergy) command. ## Basic usage To get a remove allergy event by identifier, use the `get` method on the `RemoveAllergyEvent` model manager: ```python from canvas_sdk.v1.data import RemoveAllergyEvent removal = RemoveAllergyEvent.objects.get(id="61a1853f-168f-4ed3-80d2-44e5d144bcf3") ``` If you have a patient object, the remove allergy events for a patient can be accessed with the `removed_allergies` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") removals = patient.removed_allergies.all() ``` The same records are reachable from the note they were recorded on, with the `removed_allergies` attribute on a `Note` object: ```python from canvas_sdk.v1.data.note import Note note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") removals = note.removed_allergies.all() ``` You can also access the removed allergy with the `allergy` attribute: ```python from canvas_sdk.v1.data import RemoveAllergyEvent removal = RemoveAllergyEvent.objects.get(id="61a1853f-168f-4ed3-80d2-44e5d144bcf3") allergy = removal.allergy ``` Or for a given allergy, you can access all of its removal events with the `remove_allergy_events` attribute: ```python from canvas_sdk.v1.data import AllergyIntolerance allergy = AllergyIntolerance.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") removals = allergy.remove_allergy_events.all() ``` ## Committed records The `committed` method returns remove allergy events that have been committed and not entered in error: ```python from canvas_sdk.v1.data import RemoveAllergyEvent committed_removals = RemoveAllergyEvent.objects.committed() ``` ## Attributes ### RemoveAllergyEvent Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) allergy| [AllergyIntolerance](/sdk/data-allergy-intolerance) rationale| String --- # Remove Past Medical History Event Source: https://docs.canvasmedical.com/sdk/data-remove-past-medical-history-event/ ## Introduction The `RemovePastMedicalHistoryEvent` model records an entry being removed from a patient's past medical history. It is the anchor for the [Remove Past Medical History](/sdk/commands/#remove-past-medical-history) command. Use it to find which past medical history entries were removed, on which note, and why. ## Basic usage To get a remove past medical history event by identifier, use the `get` method on the `RemovePastMedicalHistoryEvent` model manager: ```python from canvas_sdk.v1.data import RemovePastMedicalHistoryEvent removal = RemovePastMedicalHistoryEvent.objects.get(id="61a1853f-168f-4ed3-80d2-44e5d144bcf3") ``` If you have a patient object, the remove past medical history events for a patient can be accessed with the `removed_past_medical_history` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") removals = patient.removed_past_medical_history.all() ``` The same records are reachable from the note they were recorded on, with the `removed_past_medical_history` attribute on a `Note` object: ```python from canvas_sdk.v1.data.note import Note note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") removals = note.removed_past_medical_history.all() ``` You can also access the removed entry with the `condition` attribute: ```python from canvas_sdk.v1.data import RemovePastMedicalHistoryEvent removal = RemovePastMedicalHistoryEvent.objects.get(id="61a1853f-168f-4ed3-80d2-44e5d144bcf3") condition = removal.condition ``` Or for a given condition, you can access all of its removal events with the `past_medical_history_removals` attribute: ```python from canvas_sdk.v1.data import Condition condition = Condition.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") removals = condition.past_medical_history_removals.all() ``` ## Filtering The `committed` method returns remove past medical history events that have been committed and not entered in error. The `for_patient` method returns the events for one patient. You can chain them: ```python from canvas_sdk.v1.data import RemovePastMedicalHistoryEvent committed_removals = RemovePastMedicalHistoryEvent.objects.committed() patient_removals = RemovePastMedicalHistoryEvent.objects.for_patient( "1eed3ea2a8d546a1b681a2a45de1d790" ).committed() ``` ## Attributes ### RemovePastMedicalHistoryEvent Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| originator| [CanvasUser](/sdk/data-canvasuser)| committer| [CanvasUser](/sdk/data-canvasuser)| entered_in_error| [CanvasUser](/sdk/data-canvasuser)| patient| [Patient](/sdk/data-patient/#patient)| note| [Note](/sdk/data-note)| condition| [Condition](/sdk/data-condition)| The removed past medical history entry. rationale| String| The reason for the removal, up to 512 characters. Empty when none was given. --- # Resolve Condition Event Source: https://docs.canvasmedical.com/sdk/data-resolve-condition-event/ ## Introduction The `ResolveConditionEvent` model represents a record of a condition being resolved — the anchor for the [Resolve Condition](/sdk/commands/#resolve-condition) command. ## Basic usage To get a resolve condition event by identifier, use the `get` method on the `ResolveConditionEvent` model manager: ```python from canvas_sdk.v1.data import ResolveConditionEvent resolution = ResolveConditionEvent.objects.get(id="61a1853f-168f-4ed3-80d2-44e5d144bcf3") ``` If you have a patient object, the resolve condition events for a patient can be accessed with the `resolved_conditions` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") resolutions = patient.resolved_conditions.all() ``` The same records are reachable from the note they were recorded on, with the `resolved_conditions` attribute on a `Note` object: ```python from canvas_sdk.v1.data.note import Note note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") resolutions = note.resolved_conditions.all() ``` You can also access the resolved condition with the `condition` attribute: ```python from canvas_sdk.v1.data import ResolveConditionEvent resolution = ResolveConditionEvent.objects.get(id="61a1853f-168f-4ed3-80d2-44e5d144bcf3") condition = resolution.condition ``` Or for a given condition, you can access all of its resolutions with the `resolutions` attribute: ```python from canvas_sdk.v1.data import Condition condition = Condition.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") resolutions = condition.resolutions.all() ``` ## Committed records The `committed` method returns resolve condition events that have been committed and not entered in error: ```python from canvas_sdk.v1.data import ResolveConditionEvent committed_resolutions = ResolveConditionEvent.objects.committed() ``` ## Attributes ### ResolveConditionEvent Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) condition| [Condition](/sdk/data-condition) rationale| String show_in_condition_list| Boolean --- # ServiceProvider Source: https://docs.canvasmedical.com/sdk/data-serviceprovider/ ## Introduction A `ServiceProvider` is an external provider or organization — someone outside your practice that a patient's care touches. The same record backs every surface in Canvas where an outside provider gets picked: - The contact selected as the recipient of a [Refer](/sdk/commands/#refer) command. - The imaging center selected on an [Imaging Order](/sdk/commands/#imagingorder) command. - The supplier selected in **Send to** on an Order DME command. - An external care team member added to a patient's profile. - The recipient of an outbound fax, and the matched sender of an inbound one — Data Integration looks up the sending fax number in the contact directory and links the resulting provider to the incoming document, which is what the `integration_tasks` relation below exposes. Service providers come from two places. Most are drawn from the shared external contact directory, which is what those surfaces search by default and which you can query yourself with [`GET /contacts/`](/sdk/utils/#searching-for-contacts-and-service-providers). You can also build your own directory: providers created through the [ServiceProvider effect](/sdk/effect-service-provider/) belong to your instance and are flagged with `is_customer_managed`. Your own providers are not searched automatically. To offer them in one of the surfaces above, handle that surface's search event and return them yourself — see the helpers under Search results below, and [Offering your own providers alongside the directory](/guides/customize-search-results/#offering-your-own-providers-alongside-the-directory) for a worked handler covering all five surfaces. ## Basic usage To retrieve a `ServiceProvider` by identifier, use the `get` method on the model manager: ```python from canvas_sdk.v1.data.service_provider import ServiceProvider service_provider = ServiceProvider.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") ``` To retrieve a service provider from an `ImagingOrder` or a `Referral` ```python from canvas_sdk.v1.data.imaging import ImagingOrder from canvas_sdk.v1.data.referral import Referral imaging_order = ImagingOrder.objects.get(id="9d2e0f58-338b-11ec-8d3d-0242ac130003") imaging_order_service_provider = imaging_order.imaging_center referral = Referral.objects.get(id="9d2e0f58-338b-11ec-8d3d-0242ac130004") referral_service_provider = referral.service_provider ``` To show a `ServiceProvider` full name or full name with specialty use the properties `full_name` or `full_name_and_specialty` ```python from canvas_sdk.v1.data.service_provider import ServiceProvider service_provider = ServiceProvider.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") full_name = service_provider.full_name full_name_and_specialty = service_provider.full_name_and_specialty ``` ## Service Provider ### Fields Name| Type| Description ---|---|--- id| UUID| Unique identifier dbid| Integer| Internal database identifier created| DateTime| When the record was created modified| DateTime| When the record was last changed first_name| String| Provider name, or the organization name last_name| String| Empty for organizations business_fax| String| business_phone| String| business_address| String| specialty| String| Free text practice_name| String| notes| String| is_active| Boolean| `False` once deactivated; the provider is kept, not deleted npi| String| 10 digits direct_address| String| Direct address is_customer_managed| Boolean| `True` for providers created through the SDK — see below science_contact_id| Integer| The shared directory contact this provider came from, or `None` if it came from none. Not a reliable provenance signal on its own — providers that predate this tracking have no value — so use `is_customer_managed` to identify a customer's own providers. imaging_orders| QuerySet[[ImagingOrder](/sdk/data-imaging/#imagingorder)]| Imaging orders sent to this provider dme_orders| QuerySet[[DmeOrder](/sdk/data-dme/#dmeorder)]| DME orders sent to this provider referrals| QuerySet[[Referral](/sdk/data-referral/#referral)]| Referrals sent to this provider integration_tasks| QuerySet[[IntegrationTask](/sdk/data-integration-task/#integrationtask)]| Integration tasks associated with this provider ## Customer-managed providers `is_customer_managed` is `True` for providers created with the [Service Provider effects](/sdk/effect-service-provider/), and `False` for everything else. ```python from canvas_sdk.v1.data.service_provider import ServiceProvider ServiceProvider.objects.filter(is_customer_managed=True, is_active=True) ``` This is also how you find an existing customer-managed provider — to get its `id` — before updating or deactivating it with the [Service Provider effects](/sdk/effect-service-provider/). ## Search results Two helpers shape a provider for the provider-search surfaces, so you do not have to build the payloads yourself. Both take an optional list of annotations, shown next to the result. Method| Use it for ---|--- `as_search_result(annotations=None)`| A command's provider search — `Refer to` on Refer, `Imaging center` on Imaging Order, `Send to` on Order DME `as_search_contact(annotations=None)`| The fax recipient and external care team dropdowns Both identify the provider, so selecting one attaches to that exact record. The two helpers return different shapes, and the key casing below matches the serialized payload exactly: - `as_search_result(annotations=None)` returns `text`, `value`, `description`, and `annotations` at the top level, plus an `extra.contact` object containing `service_provider_id`, `science_contact_id`, `firstName`, `lastName`, `businessFax`, `businessPhone`, `businessAddress`, `specialty`, `practiceName`, and `notes`. - `as_search_contact(annotations=None)` returns a flat object: `id`, `serviceProviderId`, `firstName`, `lastName`, `practiceName`, `specialty`, `businessAddress`, `businessPhone`, `businessFax`, and `annotations`. ```python import json from canvas_sdk.effects import Effect, EffectType from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler from canvas_sdk.v1.data.service_provider import ServiceProvider class OwnDirectoryFirst(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.FAX__RECIPIENT__PRE_SEARCH), EventType.Name(EventType.PATIENT_PROFILE__EXTERNAL_CARE_TEAM__PRE_SEARCH)] def compute(self): term = self.event.context.get("search_term", "").strip() if not term: return [] providers = ServiceProvider.objects.filter( is_customer_managed=True, first_name__icontains=term ) if not providers: return [] return [ Effect( type=EffectType.AUTOCOMPLETE_SEARCH_RESULTS, payload=json.dumps( [ provider.as_search_contact( [] if provider.is_active else ["Inactive"] ) for provider in providers ] ), ) ] ``` Both surfaces reply with the same `AUTOCOMPLETE_SEARCH_RESULTS` effect. Only the event you subscribe to and the helper you call differ. What you return means different things on each surface: - **Contact dropdowns, pre-search** — results replace the search; returning nothing runs the normal search instead. - **Command searches, post-search** — an empty list clears the results, so return no effect at all when you have nothing to add. --- # Snapshot Source: https://docs.canvasmedical.com/sdk/data-snapshot/ # Snapshot Models The `Snapshot` and `SnapshotImage` models represent images captured via the Canvas iOS application or uploaded directly through the coverages modal. A `Snapshot` groups related images together, while each `SnapshotImage` represents an individual image with presigned URL support for secure access. Snapshots are primarily used for storing coverage card images. You can navigate from a `Coverage` to its `Snapshot` via the `snapshot` field, and from a `Snapshot` back to its `Coverage` via the `coverage` reverse relation. ## Basic Usage ```python from canvas_sdk.v1.data import Snapshot, SnapshotImage # Get all snapshots snapshots = Snapshot.objects.all() # Get a specific snapshot snapshot = Snapshot.objects.get(dbid=42) # Get images for a snapshot images = snapshot.images.all() # Get all snapshot images all_images = SnapshotImage.objects.all() # Get all snapshot images for a specific coverage from canvas_sdk.v1.data.coverage import Coverage coverage = Coverage.objects.get(id="a74592ae8a6c4d0ebe0799d3fb3713d1") if coverage.snapshot: images = coverage.snapshot.images.all() for image in images: print(image.image_url) ``` ## Accessing Image Files The `image_url` property on `SnapshotImage` returns a presigned S3 URL for securely accessing the image file. ```python from canvas_sdk.v1.data import SnapshotImage image = SnapshotImage.objects.exclude(image="").first() # Returns a presigned S3 URL (valid for 1 hour) url = image.image_url ``` ## Attributes ### Snapshot Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) title| String description| String coverage| [Coverage](/sdk/data-coverage/#coverage) images| SnapshotImage[] ### SnapshotImage Field Name| Type| Description ---|---|--- dbid| Integer| created| DateTime| modified| DateTime| snapshot| Snapshot| image| String| title| String| instruction| String| tag| String| image_url| String (computed)| Presigned S3 URL --- # Specialty Report Template Source: https://docs.canvasmedical.com/sdk/data-specialty-report-template/ ## Introduction The `SpecialtyReportTemplate`, `SpecialtyReportTemplateField`, and `SpecialtyReportTemplateFieldOption` models represent the templates used for specialty and referral reports. Templates define the structure of a specialty report, including what fields need to be filled in and what options are available for each field. Each template can be associated with a medical specialty via taxonomy codes. ## Basic Usage To retrieve a `SpecialtyReportTemplate` by identifier, use the `get` method on the model manager: ```python from canvas_sdk.v1.data.specialty_report_template import SpecialtyReportTemplate template = SpecialtyReportTemplate.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") ``` To access the fields defined in a template: ```python from canvas_sdk.v1.data.specialty_report_template import SpecialtyReportTemplate template = SpecialtyReportTemplate.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") fields = template.fields.all() ``` ## Filtering Templates can be filtered by any attribute on the models. ### By active status ```python from canvas_sdk.v1.data.specialty_report_template import SpecialtyReportTemplate active_templates = SpecialtyReportTemplate.objects.active() ``` ### By type ```python from canvas_sdk.v1.data.specialty_report_template import SpecialtyReportTemplate # Get custom (user-created) templates custom = SpecialtyReportTemplate.objects.custom() # Get built-in (system) templates builtin = SpecialtyReportTemplate.objects.builtin() ``` ### By specialty ```python from canvas_sdk.v1.data.specialty_report_template import SpecialtyReportTemplate # Filter by specialty taxonomy code cardiology = SpecialtyReportTemplate.objects.by_specialty("207RC0000X") ``` ### By search ```python from canvas_sdk.v1.data.specialty_report_template import SpecialtyReportTemplate results = SpecialtyReportTemplate.objects.search("cardiology") ``` ## Attributes ### SpecialtyReportTemplate Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime name| String code| String code_system| String search_keywords| String active| Boolean custom| Boolean search_as| String specialty_name| String specialty_code| String specialty_code_system| String fields| SpecialtyReportTemplateField[] ### SpecialtyReportTemplateField Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime report_template| SpecialtyReportTemplate sequence| Integer code| String code_system| String label| String units| String type| String required| Boolean options| SpecialtyReportTemplateFieldOption[] ### SpecialtyReportTemplateFieldOption Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime field| SpecialtyReportTemplateField label| String key| String --- # Staff Source: https://docs.canvasmedical.com/sdk/data-staff/ ## Introduction The `Staff` model represents a staff member in a Canvas instance. To get a `Staff` object by it's identifier, use the `get` method: ```python from canvas_sdk.v1.data.staff import Staff staff = Staff.objects.get(id="4150cd20de8a470aa570a852859ac87e") ``` To list every staff member, use `Staff.objects.all()`. When you need a stable cross-system mapping, prefer the immutable `dbid` (integer) and `id` (UUID) attributes over names, which can change: ```python from canvas_sdk.v1.data.staff import Staff staff = Staff.objects.all() staff_dbids = [s.dbid for s in staff] staff_ids = [s.id for s in staff] ``` A command's `committer` is a [`CanvasUser`](/sdk/data-canvasuser), not a `Staff` record, so its `dbid` is the user's `dbid` — not the staff `dbid`. To resolve a committer back to the `Staff` record, look it up by `user`, not `dbid`: ```python from canvas_sdk.v1.data.staff import Staff # command_instance.committer is a CanvasUser staff = Staff.objects.get(user=command_instance.committer) ``` `Staff` objects are commonly used in related models, for example the `Task` model. To see all of a staff member's assigned or created tasks, the following code can be used: ```python from canvas_sdk.v1.data.staff import Staff staff = Staff.objects.get(id="4150cd20de8a470aa570a852859ac87e") staff.assignee_tasks.all() # ]> staff.creator_tasks.all() # ]> ``` To show a Staff member's contact points (email, phone, etc.), the `telecom` attribute can be used. For example: ```python from canvas_sdk.v1.data.staff import Staff staff = Staff.objects.get(id="4150cd20de8a470aa570a852859ac87e") [(t.system, t.value,) for t in staff.telecom.all()] # [('phone', '8005551416'), ('email', 'support@canvasmedical.com')] ``` To show a `Staff` full name, credentialed name, the topmost clinical role or top role abbreviation use the properties `full_name`, `credentialed_name`, `top_clinical_role` or `top_role_abbreviation`. ```python from canvas_sdk.v1.data.staff import Staff staff = Staff.objects.get(id="4150cd20de8a470aa570a852859ac87e") staff.full_name # Larry Weed staff.credentialed_name # Larry Weed MD staff.top_clinical_role.name # Physician staff.top_role_abbreviation # MD ``` When a staff member holds more than one role, `top_clinical_role` looks only at roles in a clinical domain — those whose `domain` is `CLINICAL` or `HYBRID` — and returns the one with the highest `domain_privilege_level`. Administrative roles are never selected, even if they carry a higher privilege level. If the staff member has no clinical or hybrid roles, both `top_clinical_role` and `top_role_abbreviation` are `None`. Because `credentialed_name` appends `top_role_abbreviation`, it reflects the same highest-privilege clinical role. To get `Staff` licenses. ```python from canvas_sdk.v1.data.staff import Staff staff = Staff.objects.get(id="4150cd20de8a470aa570a852859ac87e") staff.licenses.all() # ]> ``` ## Accessing the staff signature The `signature_url` property returns a presigned S3 URL for securely accessing the staff member's signature file, when one is on file. If no signature has been uploaded, the property returns `None`. ```python from canvas_sdk.v1.data.staff import Staff staff = Staff.objects.get(id="4150cd20de8a470aa570a852859ac87e") # Returns a presigned S3 URL (valid for 1 hour) or None url = staff.signature_url ``` ## Attributes ### Staff Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| prefix| String| suffix| String| first_name| String| middle_name| String| last_name| String| maiden_name| String| nickname| String| previous_names| JSON| birth_date| Date| sex_at_birth| [PersonSex](/sdk/data-enumeration-types/#personsex)| sexual_orientation_term| String| sexual_orientation_code| String| gender_identity_term| String| gender_identity_code| String| preferred_pronouns| String| biological_race_codes| Array[String]| biological_race_terms| Array[String]| cultural_ethnicity_codes| Array[String]| cultural_ethnicity_terms| Array[String]| last_known_timezone| TimeZone| active| Boolean| primary_practice_location| [PracticeLocation](/sdk/data-practicelocation/)| npi_number| String| nadean_number| String| group_npi_number| String| bill_through_organization| Boolean| tax_id| String| tax_id_type| [TaxIDType](/sdk/data-enumeration-types/#taxidtype)| spi_number| String| personal_meeting_room_link| URL| language| Language| language_secondary| Language| schedule_column_ordering| Integer| state| JSON| user| [CanvasUser](/sdk/data-canvasuser)| signature| File (S3 key)| supervising_team| Staff[]| default_supervising_provider| Staff| notes| Note[]| supervised_notes| Note[]| creator_tasks| [Task](/sdk/data-task/#task)[]| assignee_tasks| [Task](/sdk/data-task/#task)[]| comments| [TaskComment](/sdk/data-task/#taskcomment)[]| care_team_memberships| [CareTeamMembership](/sdk/data-care-team/#careteammembership)[]| teams| [Team](/sdk/data-team/#team)[]| telecom| StaffContactPoint[]| external_identifiers| StaffExternalIdentifier[]| metadata| StaffMetadata[]| addresses| StaffAddress[]| photos| StaffPhoto[]| roles| StaffRole[]| licenses| StaffLicense[]| letters| [Letter](/sdk/data-letter/#letter)[]| imaging_orders| [ImagingOrder](/sdk/data-imaging/#imagingorder)[]| dme_orders| [DmeOrder](/sdk/data-dme/#dmeorder)[]| immunizations_given| [Immunization](/sdk/data-immunization/#immunization)[]| supervising_prescriptions| [Prescription](/sdk/data-prescription/#prescription)[]| refill_requests| [RefillRequest](/sdk/data-refill-request/#refillrequest)[]| default_patients| [Patient](/sdk/data-patient/#patient)[]| medication_history_responses| [MedicationHistoryResponse](/sdk/data-medication-history/#medicationhistoryresponse)[]| transmissions_delivered| [MessageTransmission](/sdk/data-message/#messagetransmission)[]| integration_task_reviews| [IntegrationTaskReview](/sdk/data-integration-task/#integrationtaskreview)[]| assignee_note_tasks| [NoteTask](/sdk/data-task/#notetask)[]| appointment_set| [Appointment](/sdk/data-appointment/#appointment)[]| prescription_set| [Prescription](/sdk/data-prescription/#prescription)[]| note_set| [Note](/sdk/data-note/#note)[]| prescription_change_requests| [PrescriptionChangeRequest](/sdk/data-prescription-change-request/#prescriptionchangerequest)[]| google_calendar_busy_blocks| [GoogleCalendarBusyBlock](/sdk/data-google-calendar/#googlecalendarbusyblock)[]| google_calendar_connection| [GoogleCalendarConnection](/sdk/data-google-calendar/#googlecalendarconnection)| full_name| String (computed)| The staff member's first and last name, for example `Larry Weed` credentialed_name| String (computed)| The staff member's full name suffixed with their topmost credential abbreviation, for example `Larry Weed MD` top_clinical_role| StaffRole (computed)| The staff member's highest-ranking clinical role, selected by privilege level when they hold more than one, or `None` if they have no clinical role top_role_abbreviation| String (computed)| The public credential abbreviation of `top_clinical_role`, for example `MD`, or `None` if there is no clinical role photo_url| String (computed)| The URL of the staff member's photo, or a placeholder image URL signature_url| String (computed)| A presigned S3 URL for the staff member's signature file, valid for 1 hour, or `None` if no signature is on file ### StaffContactPoint Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime system| [ContactPointSystem](/sdk/data-enumeration-types/#contactpointsystem) value| String use| String use_notes| String rank| Integer state| [ContactPointState](/sdk/data-enumeration-types/#contactpointstate) staff| Staff ### StaffAddress Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime line1| String line2| String city| String district| String state_code| String postal_code| String use| [AddressUse](/sdk/data-enumeration-types/#addressuse) type| [AddressType](/sdk/data-enumeration-types/#addresstype) longitude| Float latitude| Float start| Date end| Date country| String state| String staff| Staff ### StaffLicense Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime staff| Staff issuing_authority_long_name| String issuing_authority_url| URL license_or_certification_identifier| String issuance_date| Date expiration_date| Date license_type| LicenseType primary| Boolean state| String ### StaffPhoto Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime staff| Staff url| String title| String ### StaffRole Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime staff| Staff internal_code| String public_abbreviation| String domain| RoleDomain name| String domain_privilege_level| Integer permissions| JSON role_type| RoleType ### StaffExternalIdentifier Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime staff| Staff use| String identifier_type| String system| String value| String issued_date| Date expiration_date| Date ```python from canvas_sdk.v1.data.staff import Staff from logger import log staff_id = "4150cd20de8a470aa570a852859ac87e" staff = Staff.objects.get(id=staff_id) for identifier in staff.external_identifiers.all(): log.info(f"Staff external identifier: {identifier.system}, {identifier.value}") # https://www.example.com - employee-001 ``` `StaffExternalIdentifier` is the supported way to store and read an external system's ID on a Staff/Practitioner record (the equivalent of `PatientExternalIdentifier` for staff). Plugins read it from this data module and write to it with the [`CreateStaffExternalIdentifier`](/sdk/effect-staff-external-identifier/) effect. The FHIR `Practitioner.identifier` field carries only the NPI: Canvas stores the first identifier's value as the practitioner's NPI and ignores the rest, so an external ID sent there would replace the NPI. Use `StaffExternalIdentifier` instead. ### StaffMetadata Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime staff| Staff key| String value| String ```python from canvas_sdk.v1.data.staff import Staff from logger import log staff_id = "4150cd20de8a470aa570a852859ac87e" staff = Staff.objects.get(id=staff_id) for metadata in staff.metadata.all(): log.info(f"{metadata.key}={metadata.value}") ``` `StaffMetadata` is a free-form key/value store on a staff member, mirroring `PatientMetadata`. The `(staff, key)` pair is unique, so a given key has at most one value per staff member; use the [`StaffMetadata` effect](/sdk/effect-staff-metadata/) to upsert it from a plugin. ## Enumeration types ### License Type Value| Description ---|--- CLIA| CLIA DEA| DEA PTAN| PTAN STATE_LICENSE| State License TAXONOMY| Taxonomy SPI| SPI OTHER| Other ### Role Domain Value| Abbreviation| Description ---|---|--- CLINICAL| CLI| Clinical ADMINISTRATIVE| ADM| Administrative HYBRID| HYB| Hybrid ### Role Type Value| Description ---|--- NON_LICENSED| Non-Licensed LICENSED| Licensed PROVIDER| Provider --- # Stop Medication Event Source: https://docs.canvasmedical.com/sdk/data-stop-medication-event/ ## Introduction The `StopMedicationEvent` model represents a record of a Stop Medication Event, when a medication is removed from a patient's medication list. ## Basic usage To get a stop medication event by identifier, use the `get` method on the `StopMedicationEvent` model manager: ```python from canvas_sdk.v1.data import StopMedicationEvent stopped_medication = StopMedicationEvent.objects.get(id="61a1853f-168f-4ed3-80d2-44e5d144bcf3") ``` If you have a patient object, the stop medication events for a patient can be accessed with the `stopped_medications` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") stopped_medications = patient.stopped_medications.all() ``` You can also access the referenced medication with the `medication` attribute: ```python from canvas_sdk.v1.data import StopMedicationEvent stopped_medication = StopMedicationEvent.objects.get(id="61a1853f-168f-4ed3-80d2-44e5d144bcf3") medication = stopped_medication.medication ``` Or for a given medication, you can access all stop events: ```python from canvas_sdk.v1.data import Medication medication = Medication.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") stopped_medication_events = medication.stopmedicationevent_set.all() ``` ## Committed records The `committed` method returns stop medication events that have been committed and not entered in error: ```python from canvas_sdk.v1.data import StopMedicationEvent committed_stop_medication_events = StopMedicationEvent.objects.committed() ``` ## Attributes ### StopMedicationEvent Field Name| Type ---|--- id| UUID dbid| Integer patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) assessment| [Assessment](/sdk/data-assessment/#assessment) medication| [Medication](/sdk/data-medication) entered_in_error| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) originator| [CanvasUser](/sdk/data-canvasuser) created| DateTime modified| DateTime rationale| String --- # Task Source: https://docs.canvasmedical.com/sdk/data-task/ ## Introduction A `Task` represents a to-do item to be addressed. Tasks can be assigned to individual staff members and can also have associated comments and labels. ## Basic usage To get a task by it's identifier, use the `get` method on the `Task` model manager: ```python from canvas_sdk.v1.data.task import Task task = Task.objects.get(id="7895e1db-f8de-4660-a0a3-9e5b43a475c6") ``` From a `Patient` object, tasks for the patient can be accessed with the `tasks` attribute: ```python import arrow from canvas_sdk.v1.data.patient import Patient from canvas_sdk.v1.data.task import TaskStatus patient = Patient.objects.get(id="36950971cb3e4174ad8b9d365abfd6d0") # All tasks for the patient tasks_for_patient = patient.tasks.all() # Tasks for the patient that are overdue tasks_for_patient_overdue = patient.tasks.filter(due__lte=arrow.utcnow().datetime, status=TaskStatus.OPEN) ``` `Task` objects are also able to have associated `TaskLabel` objects. ```python from canvas_sdk.v1.data.task import Task task = Task.objects.get(id="7895e1db-f8de-4660-a0a3-9e5b43a475c6") [(label.name, label.color,) for label in task.labels.all()] # [('Emergent', 'red')] ``` `Staff` members are able to leave comments on tasks. These are stored as associated `TaskComment` objects. For example: ```python from canvas_sdk.v1.data.task import Task task = Task.objects.get(id="7895e1db-f8de-4660-a0a3-9e5b43a475c6") [(comment.creator, comment.body,) for comment in task.comments.all()] # [(, "Please call patient.")] ``` ### Note Tasks and Initial Comments A `NoteTask` represents the link between a Task command and the `Task` it generates. When a task is created via a Task command, a `NoteTask` record is created that stores the original values entered in the command. Of importance is the **initial comment** that is provided during task creation in the `internal_comment` field. This is important because `task.comments.all()` only returns manual comments added after the task is created through the interface—it does not include the original comment entered during task creation. To access that initial comment, you need to use the `NoteTask` model. To get a note task by its identifier: ```python from canvas_sdk.v1.data.task import NoteTask note_task = NoteTask.objects.get(id="a1b2c3d4-e5f6-7890-abcd-ef1234567890") print(f"Initial comment: {note_task.internal_comment}") ``` From a `Task` object, you can access the associated `NoteTask` to retrieve the initial comment: ```python from canvas_sdk.v1.data.task import Task task = Task.objects.get(id="7895e1db-f8de-4660-a0a3-9e5b43a475c6") # Access the NoteTask to get the initial comment note_task = task.note_tasks.first() if note_task: print(f"Initial comment: {note_task.internal_comment}") print(f"Original title: {note_task.original_title}") print(f"Original assignee: {note_task.original_assignee}") ``` Common workflow pattern: handling a TASK_CREATED event and accessing the initial comment: ```python from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data.task import Task class TaskCreatedHandler(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.TASK_CREATED)] def compute(self): task_id = self.target task = Task.objects.get(id=task_id) # Get the initial comment from the NoteTask note_task = task.note_tasks.first() if note_task: initial_comment = note_task.internal_comment # Use the initial comment for your logic self.log(f"Task created with initial comment: {initial_comment}") ``` From a `Note` object, note tasks can be accessed with the `note_tasks` attribute: ```python from canvas_sdk.v1.data.note import Note note = Note.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") note_tasks = note.note_tasks.all() for note_task in note_tasks: print(f"Task: {note_task.original_title}") print(f"Initial comment: {note_task.internal_comment}") ``` ## Committed note tasks The `committed` method on the `NoteTask` model manager returns note tasks whose underlying Task command has been committed and not entered in error: ```python from canvas_sdk.v1.data.task import NoteTask committed_note_tasks = NoteTask.objects.committed() ``` ## Attributes ### Task Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime creator| [Staff](/sdk/data-staff/#staff) assignee| [Staff](/sdk/data-staff/#staff) patient| [Patient](/sdk/data-patient/#patient) team| [Team](/sdk/data-team/) task_type| TaskType tag| String title| String due| DateTime due_event| EventType status| TaskStatus priority| TaskPriority comments| TaskComment[] labels| TaskLabel[] metadata| TaskMetadata[] note_tasks| NoteTask[] ### NoteTask Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser/) committer| [CanvasUser](/sdk/data-canvasuser/) entered_in_error| [CanvasUser](/sdk/data-canvasuser/) note| [Note](/sdk/data-note/#note) assessment| [Assessment](/sdk/data-assessment/#assessment) task| Task patient| [Patient](/sdk/data-patient/#patient) original_title| String original_assignee| [Staff](/sdk/data-staff/#staff) original_team| [Team](/sdk/data-team/) original_role| [CareTeamRole](/sdk/data-care-team/#careteamrole) original_due| DateTime internal_comment| String ### TaskComment Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime creator| [Staff](/sdk/data-staff/#staff) task| [Task](/sdk/data-task/#task) body| String referral| [Referral](/sdk/data-referral/) ### TaskLabel Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime tasks| M2M position| Integer color| [ColorEnum](/sdk/data-enumeration-types/#colorenum) task_association| [Origin](/sdk/data-enumeration-types/#origin) name| String active| Boolean modules| TaskLabelModule claims| [Claim](/sdk/data-claim)[] appointments| [Appointment](/sdk/data-appointment/)[] ### TaskTaskLabel The join between a task and a TaskLabel. Reach the labels on a task through its `labels` attribute rather than querying this model. Field Name| Type ---|--- dbid| Integer task| Task task_label| TaskLabel ### TaskMetadata Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime task| Task key| String value| String ```python from canvas_sdk.v1.data.task import Task from logger import log task_id = "7895e1db-f8de-4660-a0a3-9e5b43a475c6" task = Task.objects.get(id=task_id) task_metadata = task.metadata.all() for metadata in task_metadata: log.info(f"Task metadata: {metadata.key}, {metadata.value}") # external_system_id - EXT-12345 ``` ## Enumeration types ### TaskType Value| Label ---|--- Task| Task Reminder| Reminder ### EventType Value| Label ---|--- Chart Open| Chart Open ### TaskStatus Value| Label ---|--- COMPLETED| Completed CLOSED| Closed OPEN| Open ### TaskPriority Value| Label ---|--- STAT| STAT URGENT| Urgent ROUTINE| Routine ### TaskLabelModule Value| Label ---|--- claims| Claims tasks| Tasks appointments| Appointments --- # Team Source: https://docs.canvasmedical.com/sdk/data-team/ ## Introduction The `Team` model represents a team of staff members in a Canvas instance. ## Basic usage To get an team by identifier, use the `get` method on the `Team` model manager: ```python from canvas_sdk.v1.data.team import Team team = Team.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a staff object, the teams that a staff is a member of can be accessed with the `teams` attribute on a `Staff` object: ```python from canvas_sdk.v1.data.staff import Staff staff = Staff.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") teams = staff.teams.all() ``` ## Team Members The members of a team can be access with the `members` attribute on a `Team` object: ```python from canvas_sdk.v1.data.team import Team from logger import log team = Team.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") for member in team.members.all(): log.info(f"first_name: {member.first_name}") log.info(f"last_name: {member.last_name}") ``` ## Reconciling with FHIR A team's `group_id` is the same identifier used to represent the team in the [FHIR Group endpoint](/api/group/). Use it to cross-reference a `Team` between the SDK and FHIR. Given a `Team`, you can use its `group_id` with the [Canvas FHIR client](/sdk/clients-canvas-fhir/) to fetch the corresponding FHIR `Group` payload: ```python from canvas_sdk.clients.canvas_fhir import CanvasFhir from canvas_sdk.v1.data.team import Team team = Team.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") # Declare these secrets in the CANVAS_MANIFEST.json and set the values on the # plugin configuration page. client = CanvasFhir( self.secrets["CANVAS_FHIR_CLIENT_ID"], self.secrets["CANVAS_FHIR_CLIENT_SECRET"], ) # Use the team's group_id to read the corresponding FHIR Group resource. group = client.read("Group", str(team.group_id)) ``` ## Filtering Teams can be filtered by any attribute that exists on the model. Filtering for teams is done with the `filter` method on the `Team` model manager. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.team import Team teams = Team.objects.filter(created__gt="2025-01-01") ``` ## Attributes ### Team Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime name| String responsibilities| Array[TeamResponsibility] members| [Staff](/sdk/data-staff/#staff)[] group_id| UUID telecom| TeamContactPoint[] tasks| [Task](/sdk/data-task/#task)[] document_references| [DocumentReference](/sdk/data-document-reference/#documentreference)[] integration_task_team_reviews| [IntegrationTaskReview](/sdk/data-integration-task/#integrationtaskreview)[] uncategorizedclinicaldocument_set| [UncategorizedClinicalDocument](/sdk/data-uncategorized-clinical-document/#uncategorizedclinicaldocument)[] referralreport_set| [ReferralReport](/sdk/data-referral/#referralreport)[] assigned_messages| [Message](/sdk/data-message/#message)[] labreport_set| [LabReport](/sdk/data-labs/#labreport)[] imagingreport_set| [ImagingReport](/sdk/data-imaging/#imagingreport)[] patientadministrativedocument_set| [PatientAdministrativeDocument](/sdk/data-patient-administrative-document/#patientadministrativedocument)[] ### TeamContactPoint Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime system| [ContactPointSystem](/sdk/data-enumeration-types/#contactpointsystem) value| String use| [ContactPointUse](/sdk/data-enumeration-types/#contactpointuse) use_notes| String rank| Integer state| [ContactPointState](/sdk/data-enumeration-types/#contactpointstate) team| Team ## Enumeration types ### TeamResponsibility Field Name| Type ---|--- COLLECT_SPECIMENS_FROM_PATIENT| Collect specimens from a patient COMMUNICATE_DIAGNOSTIC_RESULTS_TO_PATIENT| Communicate diagnostic results to patient COORDINATE_REFERRALS_FOR_PATIENT| Coordinate referrals for a patient PROCESS_REFILL_REQUESTS| Process refill requests from a pharmacy PROCESS_CHANGE_REQUESTS| Process change requests from a pharmacy SCHEDULE_LAB_VISITS_FOR_PATIENT| Schedule lab visits for a patient POPULATION_HEALTH_CAMPAIGN_OUTREACH| Population health campaign outreach COLLECT_PATIENT_PAYMENTS| Collect patient payments COMPLETE_OPEN_LAB_ORDERS| Complete open lab orders REVIEW_ERA_POSTING_EXCEPTIONS| Review electronic remittance posting exceptions REVIEW_COVERAGES| Review incomplete patient coverages --- # Uncategorized Clinical Document Source: https://docs.canvasmedical.com/sdk/data-uncategorized-clinical-document/ ## Introduction The `UncategorizedClinicalDocument` and `UncategorizedClinicalDocumentReview` models represent uncategorized clinical documents and their reviews. ## Basic Usage ```python from canvas_sdk.v1.data import UncategorizedClinicalDocument, UncategorizedClinicalDocumentReview document = UncategorizedClinicalDocument.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") review = UncategorizedClinicalDocumentReview.objects.get(id="c1a5a35a-4ee2-4a0e-85c0-21739dc8c4a8") ``` ## Filtering Uncategorized clinical documents and reviews can be filtered by any attribute that exists on the models. ### By review mode Filter documents by their review mode: ```python from canvas_sdk.v1.data import UncategorizedClinicalDocument from canvas_sdk.commands.commands.review import ReviewMode documents_to_review = UncategorizedClinicalDocument.objects.filter(review_mode=ReviewMode.REVIEW_REQUIRED) ``` ### Unreviewed documents To get uncategorized documents that have not been reviewed yet and require a review: ```python from canvas_sdk.v1.data import UncategorizedClinicalDocument from canvas_sdk.commands.commands.review import ReviewMode from django.db.models import Q unreviewed_documents = UncategorizedClinicalDocument.objects.filter(Q(review_mode=ReviewMode.REVIEW_REQUIRED), (Q(review__committer__isnull=True) | Q(review__entered_in_error__isnull=False))) ``` ## Review assignment A document is assigned for review to staff members or to a team. `reviewers` holds the assigned staff members, and `team` holds the assigned team, or `None` when no team is assigned. When the review is delegated to another staff member, `reviewers` lists the delegate in place of the original reviewer. ```python from canvas_sdk.v1.data import UncategorizedClinicalDocument, Staff document = UncategorizedClinicalDocument.objects.get(id="c1a5a35a-4ee2-4a0e-85c0-21739dc8c4a8") # The staff members and the team the document is assigned to reviewers = document.reviewers.all() team = document.team # Every document assigned to this staff member staff = Staff.objects.get(id="4150cd20de8a470aa570a852859ac87e") assigned = UncategorizedClinicalDocument.objects.filter(reviewers__id=staff.id) ``` ## Delegations A document review can be delegated to another staff member or team. The delegations for a document are available through two accessors: ```python from canvas_sdk.v1.data import UncategorizedClinicalDocument document = UncategorizedClinicalDocument.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") # The full delegation history, oldest first. history = document.delegations # The current active delegation, or None when the document is with its owner. current = document.active_delegation ``` See [DocumentReviewDelegation](/sdk/data-document-review-delegation/) for the delegation model and the `DOCUMENT_DELEGATED` event. ## Document codings The `code` field comes from the document's type, which is drawn from a fixed list rather than set freely — either the type selected in Data Integration, or, when a document is created through the FHIR [DocumentReference](/api/documentreference/) endpoint, the LOINC code supplied in `type.coding`, which must match one of the codes below. Every coding uses the LOINC system (`http://loinc.org`). The document types stored as uncategorized clinical documents are: Document type| Code| Display ---|---|--- Care Management Documents| 91983-7| Care management note Clinical Patient Intake Form| 64285-0| Medical history screening form Emergency Department Report| 96335-5| Emergency department Summary note External Medical Records| 11503-0| Medical records Home Care Report| 75503-3| Patient's home Note Hospital Discharge Summary| 34105-7| Hospital Discharge summary Hospital History and Physical| 47039-3| Hospital Admission history and physical note Nursing Home| 34113-1| Nursing facility Note Operative Report| 11504-8| Surgical operation note Physical Exam Documents| 51848-0| Evaluation note Prescription Refill Request| 57833-6| Prescription for medication Rehabilitation Report| 34823-5| Physical medicine and rehab Note Uncategorized Clinical Document| 34109-9| Note In Office Testing Documents| —| none > **Warning:** In Office Testing Documents have no coding assigned, so their `code` is `None`. Filtering on `code` silently excludes them, and because the FHIR endpoint identifies a document's type by its LOINC code, they can only be created through Data Integration. Administrative document types are stored as [PatientAdministrativeDocument](/sdk/data-patient-administrative-document/) instead. Lab reports, imaging reports and specialist consult reports have their own models, so their codings never appear here. ## Accessing the document file The `document_url` property returns a presigned S3 URL for the document file, or `None` when no file is present. The URL is valid for one hour, so generate it when you need it rather than storing it. ```python from canvas_sdk.v1.data import UncategorizedClinicalDocument record = UncategorizedClinicalDocument.objects.get( id="d2194110-5c9a-4842-8733-ef09ea5ead11" ) # Presigned S3 URL to the document file, or None url = record.document_url ``` ## The document reference Each record also has a [DocumentReference](/sdk/data-document-reference/#the-related-object) pointing back at it. The document reference carries the document's coding, category, and status, and represents the document in the FHIR API. Use `document_url` to get the file itself, and the document reference when you want that metadata. To find it, resolve the [ContentType](/sdk/data-content-type/) at runtime from its stable `app_label` and `model` — never hardcode the per-environment `dbid` — and match `object_id` against the record's `dbid`: ```python from canvas_sdk.v1.data import ContentType, DocumentReference, UncategorizedClinicalDocument record = UncategorizedClinicalDocument.objects.get( id="d2194110-5c9a-4842-8733-ef09ea5ead11" ) content_type = ContentType.objects.filter( app_label="api", model="uncategorizedclinicaldocument" ).first() document = DocumentReference.objects.filter( content_type=content_type, object_id=record.dbid ).first() url = document.document_url if document else None ``` > **Info:** `object_id` holds the related record's integer `dbid`, not its UUID `id`. A document marked entered-in-error keeps its document reference, with the status carried across, so check `status` if that matters to you. ## Attributes ### UncategorizedClinicalDocument Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime patient| [Patient](/sdk/data-patient/#patient) originator| [CanvasUser](/sdk/data-canvasuser) assigned_by| [CanvasUser](/sdk/data-canvasuser) review| UncategorizedClinicalDocumentReview team| [Team](/sdk/data-team/#team) reviewers| [Staff](/sdk/data-staff/#staff)[] code| [DocumentCoding](/sdk/data-patient-administrative-document/#documentcoding) name| String review_mode| [DocumentReviewMode](/sdk/data-enumeration-types/#documentreviewmode) junked| Boolean requires_signature| Boolean assigned_date| DateTime team_assigned_date| DateTime original_date| Date comment| String priority| Boolean document| File (S3 key) document_url| String (computed) ### UncategorizedClinicalDocumentReview Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) internal_comment| String message_to_patient| String status| String patient| [Patient](/sdk/data-patient/#patient) patient_communication_method| String reports| QuerySet[UncategorizedClinicalDocument] --- # Vaccine Source: https://docs.canvasmedical.com/sdk/data-vaccine/ ## Introduction The `Vaccine` model represents an entry in a Canvas instance's vaccine catalog — what a provider can choose when documenting an [Immunize](/sdk/commands/#immunize) command. `VaccineLot` represents a physical lot of one of those vaccines, along with how many doses remain on hand. ## Basic usage A vaccine carries the CPT and CVX codes that identify it. The CVX code is on the vaccine; the CPT codes come from its charges, and those charges are what produce the billing line item when an [Immunize](/sdk/commands/#immunize) command is committed. ```python from canvas_sdk.v1.data import Vaccine vaccine = Vaccine.objects.filter(active=True, cvx_code="135").first() print(vaccine.cvx_code, [charge.cpt_code for charge in vaccine.charges.all()]) # 135 ["90662"] ``` Each physical lot of a vaccine tracks how many doses remain, and committing an Immunize command decrements that count: ```python from canvas_sdk.v1.data import VaccineLot lot = VaccineLot.objects.filter(lot_number="LOT-135-001").first() print(lot.vaccine.short_name, lot.on_hand_inventory, lot.expiration_date) # Fluzone High-Dose 25 2027-06-30 ``` `mvx_code` holds a CDC MVX manufacturer code. The codes are declared as the field's choices, so Django's display helper resolves the manufacturer name: ```python from canvas_sdk.v1.data import VaccineLot lot = VaccineLot.objects.filter(lot_number="LOT-135-001").first() print(lot.mvx_code, "->", lot.get_mvx_code_display()) # ASZ -> AstraZeneca ``` Some instances record a single stock figure on the vaccine itself rather than tracking lots. That value lives in `Vaccine.inventory` as free text and is independent of `VaccineLot.on_hand_inventory`. ## Filtering A vaccine is selectable on a note when it is active **and** carries an active CPT charge. Filtering the same way keeps a plugin in step with what a provider would see: ```python from datetime import date from django.db.models import Q from canvas_sdk.v1.data import Vaccine today = date.today() selectable = Vaccine.objects.filter( Q(active=True), Q(charges__effective_date__lte=today), Q(charges__end_date__isnull=True) | Q(charges__end_date__gte=today), ).distinct() print([vaccine.short_name for vaccine in selectable]) # ["Fluzone High-Dose", "Trumenba", "Prevnar 13™"] ``` Lots are administrable while they have doses on hand: ```python from canvas_sdk.v1.data import VaccineLot in_stock = VaccineLot.objects.filter(vaccine__cvx_code="135", on_hand_inventory__gt=0) print([(lot.lot_number, lot.on_hand_inventory) for lot in in_stock]) # [("LOT-135-001", 25)] ``` ## Attributes ### Vaccine Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime payer| [Transactor](/sdk/data-coverage/#transactor) charges| QuerySet[[ChargeDescriptionMaster](/sdk/data-charge-description-master/#chargedescriptionmaster)] cvx_code| String name| String short_name| String inventory| String ndc_code| String mvx_code| VaccineManufacturer route| String active| Boolean units| Integer lots| QuerySet[VaccineLot] A vaccine may appear more than once for the same `cvx_code` — instances commonly carry a payer-specific entry alongside a general one. Use `payer` to tell them apart. ### VaccineLot Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime vaccine| Vaccine lot_number| String ndc_code| String mvx_code| VaccineManufacturer expiration_date| Date diluent_lot_number| String diluent_expiration_date| Date starting_inventory| Integer quantity_adjustment| Integer adjustment_notes| String on_hand_inventory| Integer used_inventory| Integer `on_hand_inventory` is derived from `starting_inventory + quantity_adjustment - used_inventory`. ## Enumeration types ### VaccineManufacturer CDC MVX manufacturer codes. Prefer `get_mvx_code_display()` over mapping these yourself. Value| Label ---|--- ASZ| AstraZeneca BBI| Bharat Biotech International Limited BN| Bavarian Nordic A/S BTP| Biotest Pharmaceuticals Corporation CAN| CanSino Biologics, Inc DVC| DynPort Vaccine Company, LLC DVX| Dynavax, Inc GEO| GeoVax Labs, Inc GRF| Grifols IDB| ID Biomedical JNJ| Johnson and Johnson JSN| Janssen KED| Kedrion Biopharma KGC| Korea Green Cross Corporation MBL| Massachusetts Biologic Laboratories MDO| Medicago, Inc MED| MedImmune, Inc. (AstraZeneca) MIP| Emergent BioSolutions MOD| Moderna US, Inc MSD| Merck and Co., Inc MSP| MSP Vaccine Company - (partnership Merck and Sanofi Pasteur) NAB| NABI NVX| Novavax, Inc OTH| Other manufacturer PAX| Emergent Travel Health, Inc (Formerly PaxVax) PFR| Pfizer, Inc PMC| Sanofi Pasteur PSC| Protein Sciences SEQ| Seqirus SKB| GlaxoSmithKline SNV| Sinovac SPH| Sinopharm-Biotech TVA| TEVA Pharmaceuticals USA UNK| Unknown manufacturer VAL| Valneva VBI| VBI Vaccines, Inc WAL| Wyeth --- # ValueSets Source: https://docs.canvasmedical.com/sdk/data-value-sets/ ## Introduction The Canvas SDK includes a library of built-in Value Sets that can be used within plugins to assist with finding conditions or medications related to Electronic Clinical Quality Measures. Plugin developers can also create their own Value Sets and use them in the same manner as the Canvas built-in `ValueSet` classes. Built-in Value Sets that can be imported into plugins can be found in the Canvas SDK open source repo [here](https://github.com/canvas-medical/canvas-plugins/tree/main/canvas_sdk/value_set/). ## Usage **Filtering Conditions by Value Set** Value Set classes can be used directly in the data module to query for conditions that are included within them. For example, to find if a patient has been diagnosed with a condition whose coding falls under a particular Value Set, the `find` method can be used as follows: ```python from logger import log from canvas_sdk.v1.data.patient import Patient from canvas_sdk.value_set.v2022.condition import EssentialHypertension patient = Patient.objects.get(id="6cbc40b408294a5f9b41f57ba1b2b487") patient_essential_hypertension_conditions = patient.conditions.find(EssentialHypertension) # The patient has been diagnosed with one or more conditions that match a coding within the EssentialHypertension value set if patient_essential_hypertension_conditions: for condition in patient_essential_hypertension_conditions: log.info(condition.codings.all().values()) ``` **Filtering Medications by Value Set** Similar to the `Condition` example above, the `find` method can also utilize Value Set classes to filter `Medication` records that fall under a value set: ```python from canvas_sdk.v1.data.patient import Patient from canvas_sdk.value_set.v2022.medication import DementiaMedications from logger import log patient = Patient.objects.get(id="6cbc40b408294a5f9b41f57ba1b2b487") patient_dementia_medications = patient.medications.find(DementiaMedications) if patient_dementia_medications: for medication in patient_dementia_medications: log.info(medication.codings.all().values()) ``` **Filtering with more than one Value Set** Sometimes it may be desirable to filter using more than one Value Set. For example, finding all of a patient's conditions that belong within `EssentialHypertension` _or_ `DiagnosisOfHypertension`. In this case, the `find` supports the pipe (`|`) operator to filter conditions that match the codings in either Value Set: ```python from canvas_sdk.v1.data.patient import Patient from canvas_sdk.value_set.v2022.condition import EssentialHypertension, DiagnosisOfHypertension from logger import log patient = Patient.objects.get(id="6cbc40b408294a5f9b41f57ba1b2b487") patient_hypertension_conditions = patient.conditions.find(EssentialHypertension | DiagnosisOfHypertension) if patient_hypertension_conditions: for condition in patient_hypertension_conditions: log.info(condition.codings.all().values()) ``` ## Creating Custom Value Sets The Canvas SDK allows plugin developers to create their own ValueSet classes that can be used in the same manner as the examples above. To do so, one can import and inherit the base `ValueSet` class: ```python from canvas_sdk.value_set.value_set import ValueSet ``` A new class containing Python sets of coding values can be defined like so: ```python from canvas_sdk.value_set.value_set import ValueSet class MyCustomValueSet(ValueSet): VALUE_SET_NAME = "My Custom Value Set" ICD10CM = { "T2601XA", # Burn of right eyelid and periocular area, initial encounter } SNOMEDCT = { "284537006", # Eyelid burn (disorder) } ``` The valid code system constants that can be used to define sets of codes in Value Sets are: Name| URL ---|--- `CPT`| `http://www.ama-assn.org/go/cpt` `HCPCSLEVELII`| `https://coder.aapc.com/hcpcs-codes` `CVX`| `http://hl7.org/fhir/sid/cvx` `LOINC`| `http://loinc.org` `SNOMEDCT`| `http://snomed.info/sct` `FDB`| `http://www.fdbhealth.com/` `RXNORM`| `http://www.nlm.nih.gov/research/umls/rxnorm` `ICD10`| `ICD-10` `NUCC`| `http://www.nucc.org/` `CANVAS`| `CANVAS` `INTERNAL`| `INTERNAL` `NDC`| `http://hl7.org/fhir/sid/ndc` The following code is an example of a custom `ValueSet` in use within a plugin: ```python from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from logger import log from canvas_sdk.v1.data.patient import Patient from canvas_sdk.value_set.value_set import ValueSet class MyCustomValueSet(ValueSet): VALUE_SET_NAME = "My Custom Value Set" ICD10CM = { "T2601XA", # Burn of right eyelid and periocular area, initial encounter } SNOMEDCT = { "284537006", # Eyelid burn (disorder) } class MyHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.PATIENT_UPDATED) def compute(self): patient = Patient.objects.get(id="6cbc40b408294a5f9b41f57ba1b2b487") custom_value_set_conditions = patient.conditions.find(MyCustomValueSet) for vs in custom_value_set_conditions: log.info(vs) return [] ``` --- # VisualExamFinding Source: https://docs.canvasmedical.com/sdk/data-visual-exam-finding/ ## Introduction The `VisualExamFinding` model represents a visual exam finding captured on a note. Each finding consists of a titled image along with a narrative description of the clinical observation. To record a finding from a plugin, use the [VisualExamFinding command](/sdk/commands/#visualexamfinding). ## Basic usage To get a visual exam finding by identifier, use the `get` method on the `VisualExamFinding` model manager: ```python from canvas_sdk.v1.data.visual_exam_finding import VisualExamFinding finding = VisualExamFinding.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient object, the visual exam findings for a patient can be accessed with the `visual_exam_findings` attribute on a `Patient` object: ```python from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") findings = patient.visual_exam_findings.all() ``` If you have a note object, the visual exam findings for that note can be accessed with the `visual_exam_findings` attribute on a `Note` object: ```python from canvas_sdk.v1.data.note import Note note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") findings = note.visual_exam_findings.all() ``` ## Accessing image files The `image_url` property returns a presigned S3 URL for securely accessing the image file. The URL is valid for 1 hour. ```python from canvas_sdk.v1.data.visual_exam_finding import VisualExamFinding finding = VisualExamFinding.objects.exclude(image="").first() # Returns a presigned S3 URL (valid for 1 hour), or None if no image is set url = finding.image_url ``` ## Filtering Visual exam findings can be filtered by any attribute that exists on the model. ### By attribute Specify an attribute with `filter` to filter by that attribute: ```python from canvas_sdk.v1.data.visual_exam_finding import VisualExamFinding # Get all findings with a specific title findings = VisualExamFinding.objects.filter(title="Left forearm") ``` ### By patient ```python from canvas_sdk.v1.data.visual_exam_finding import VisualExamFinding from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") findings = VisualExamFinding.objects.filter(patient=patient) ``` ### Committed findings The `committed` method returns visual exam findings that have been committed and not entered in error: ```python from canvas_sdk.v1.data.visual_exam_finding import VisualExamFinding committed_findings = VisualExamFinding.objects.committed() ``` ## Attributes ### VisualExamFinding Field Name| Type| Description ---|---|--- id| UUID| dbid| Integer| created| DateTime| modified| DateTime| patient| [Patient](/sdk/data-patient/#patient)| note| [Note](/sdk/data-note/#note)| image| File (S3 key)| title| String| narrative| String| originator| [CanvasUser](/sdk/data-canvasuser)| committer| [CanvasUser](/sdk/data-canvasuser)| entered_in_error| [CanvasUser](/sdk/data-canvasuser)| image_url| String (computed)| Presigned S3 URL --- # VitalSignReading Source: https://docs.canvasmedical.com/sdk/data-vital-sign-reading/ ## Introduction The `VitalSignReading` model is the anchor for the [Vitals](/sdk/commands/#vitals) command — a set of vital-sign readings recorded on a Note for a Patient. The individual measurements (blood pressure, heart rate, temperature, weight, etc.) are stored as related `VitalSign` records, reachable via the `signs` attribute. ## Basic usage To get a vital sign reading by identifier, use the `get` method on the `VitalSignReading` model manager: ```python from canvas_sdk.v1.data.vitals import VitalSignReading reading = VitalSignReading.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") ``` If you have a patient or note object, the readings can be accessed with the `vital_sign_readings` attribute: ```python from canvas_sdk.v1.data.patient import Patient from canvas_sdk.v1.data.note import Note patient = Patient.objects.get(id="1eed3ea2a8d546a1b681a2a45de1d790") readings = patient.vital_sign_readings.all() note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") readings = note.vital_sign_readings.all() ``` If you have a patient ID, you can get the readings for the patient with the `for_patient` method: ```python from canvas_sdk.v1.data.vitals import VitalSignReading patient_id = "1eed3ea2a8d546a1b681a2a45de1d790" readings = VitalSignReading.objects.for_patient(patient_id) ``` ## Reading the individual measurements Each `VitalSignReading` has one or more `VitalSign` measurements, accessed with the `signs` attribute. Each `VitalSign` carries the measurement's LOINC code, name, value, and units: ```python from canvas_sdk.v1.data.vitals import VitalSignReading from logger import log reading = VitalSignReading.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") for sign in reading.signs.all(): log.info(f"{sign.sign}: {sign.value} {sign.units} (LOINC {sign.loinc_num})") ``` `signs` includes the parts of a composite measurement as well as the measurement itself, so a blood pressure appears three times in the loop above. See Composite measurements to walk only the top-level readings. ## Filtering Vital sign readings can be filtered by any attribute that exists on the model. ### Committed readings The `committed` method returns readings that have been committed and not entered in error: ```python from canvas_sdk.v1.data.vitals import VitalSignReading committed_readings = VitalSignReading.objects.committed() ``` ## Attributes ### VitalSignReading Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime originator| [CanvasUser](/sdk/data-canvasuser) committer| [CanvasUser](/sdk/data-canvasuser) entered_in_error| [CanvasUser](/sdk/data-canvasuser) patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note) date_recorded| DateTime signs| VitalSign[] ### VitalSign Field Name| Type ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime reading| VitalSignReading date_recorded| DateTime loinc_num| String sign| String — one of the sign values sign_description| String value| String units| String source| String parent| VitalSign — the composite measurement this one is a part of, if any children| VitalSign[] — the parts of this measurement, if it is a composite ## Composite measurements Some measurements are recorded as a whole _and_ as their parts. The whole is stored as one `VitalSign` and each part as another, linked to it by `parent`; the reverse accessor is `children`. A measurement that stands on its own has `parent` set to `None` and no `children`. The [Vitals](/sdk/commands/#vitals) command produces two of these: - `blood_pressure` — the combined reading, parent of the `systole` and `diastole` signs taken from it. - `oxygen_saturation` — parent of `inhaled_oxygen_concentration` and `inhaled_oxygen_flow_rate`. Because the parts sit alongside the whole in `reading.signs`, iterating a reading naively counts a blood pressure three times. Filter on `parent` to walk only the top-level measurements: ```python from canvas_sdk.v1.data.vitals import VitalSignReading from logger import log reading = VitalSignReading.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") for sign in reading.signs.filter(parent__isnull=True): parts = ", ".join(f"{part.sign}={part.value}" for part in sign.children.all()) log.info(f"{sign.sign}: {sign.value} {sign.units}" + (f" ({parts})" if parts else "")) ``` ## Sign values `VitalSign.sign` holds one of a fixed set of values — the ones below are those a Canvas workflow records. Canvas declares them as a `VitalSignChoices` enumeration internally, but that enumeration is **not** exported to plugins, so compare against the string value directly: ```python from canvas_sdk.v1.data.vitals import VitalSignReading reading = VitalSignReading.objects.get(id="b80b1cdc-2e6a-4aca-90cc-ebc02e683f35") weights = [sign for sign in reading.signs.all() if sign.sign == "weight"] ``` `sign_description` carries a human-readable label for the same measurement, so prefer it for display and reserve `sign` for matching. ### Where each value comes from Not every value is produced by every workflow, so which ones you see depends on how the vitals were recorded: - **The[Vitals](/sdk/commands/#vitals) command** writes `height`, `weight`, `waist_circumference`, `body_temperature`, `blood_pressure`, `systole`, `diastole`, `pulse`, `pulse_rhythm`, `respiration_rate`, `oxygen_saturation`, `inhaled_oxygen_concentration`, `inhaled_oxygen_flow_rate`, `supplemental_oxygen` and `note`. Blood pressure is stored three times over — once as the combined `blood_pressure` reading and once each as `systole` and `diastole`. - **A committed pediatric physical exam questionnaire** records `length` and `head_circumference_tape_measure`, taken from the answers carrying those LOINC codes. Derived measurements are **not** `VitalSign` records. When a height, weight or length is recorded, Canvas calculates BMI from the height and weight and stores the results — the BMI-for-age, head-circumference and weight-for-height percentiles — as [Observation](/sdk/data-observation/) records attached to the reading, because each is computed from more than one measurement. Read them there rather than looking for a `sign`. Value| Label ---|--- blood_pressure| Blood Pressure systole| Systole diastole| Diastole pulse| Pulse pulse_rhythm| Pulse Rhythm respiration_rate| Respiration Rate body_temperature| Body Temperature oxygen_saturation| Oxygen Saturation supplemental_oxygen| Supplemental Oxygen inhaled_oxygen_concentration| Inhaled Oxygen Concentration inhaled_oxygen_flow_rate| Inhaled Oxygen Flow Rate weight| Weight height| Height length| Length head_circumference_tape_measure| Head Circumference by Tape Measure waist_circumference| Waist Circumference note| Note --- # Data Source: https://docs.canvasmedical.com/sdk/data/ The data module provides you with data to compute on. It provides curated, secure access to both PHI (e.g. patient data) and non-PHI (e.g. staff and practice-level data), representing the current state of your target Canvas instance. The module's classes offer convenience methods and operators that make business logic and clinical logic easy to express with standard terminologies like ICD-10, SNOMED-CT, CPT, and the like. Data module classes are Django ORM models, which allow easy retrieval of data at runtime through Django's expressive [QuerySet API](https://docs.djangoproject.com/en/5.1/ref/models/querysets/). Access to these models is **read-only** , and mutations to them are allowed only via use of [Effects](/sdk/effects/) and the FHIR API. Use the [Custom Data](/sdk/custom-data/) features for creating and maintaining your plugin's own data. The pages below provide listings of the models, their attributes, and examples of usage. [ AllergyIntoleranceHarmful or undesired physiological responses associated with exposure to a substance. ](/sdk/data-allergy-intolerance/)[ ApplicationA plugin application. ](/sdk/data-application/)[ AppointmentA scheduled meeting between a patient and a provider. ](/sdk/data-appointment/)[ AssessCodingGapEventA record of a coding gap being assessed, with the conditions it was assessed against. ](/sdk/data-coding-gap-event/#assesscodinggapevent)[ AssessmentClinical assessment of a patient's condition. ](/sdk/data-assessment/)[ BannerAlertAn alert notification linked to a patient. ](/sdk/data-banner-alert/)[ BillingLineItemA billable code linked to a patient note. ](/sdk/data-billing-line-item/)[ BusinessLineA group of patients that share a common brand under an organization. ](/sdk/data-business-line/)[ CalendarCalendars associated with providers. ](/sdk/data-calendar/)[ CancelPrescriptionA request to cancel a patient's prescription (the CancelPrescription command). ](/sdk/data-cancel-prescription/)[ CancelPrescriptionResponseThe response to a CancelPrescription request. ](/sdk/data-cancel-prescription-response/)[ CanvasUserUser accounts associated with other records. ](/sdk/data-canvasuser/)[ CareTeamTeams assigned for patient care. ](/sdk/data-care-team/)[ Change MedicationA record of a Change Medication command, used to update a medication's directions (sig) without issuing a new prescription. ](/sdk/data-change-medication/)[ ChargeDescriptionMasterBilling charges in Canvas that can be added to the note footer. ](/sdk/data-charge-description-master/)[ ChartSectionReviewReviewed chart sections captured on a note, with their pre-rendered title and narrative content. ](/sdk/data-chart-section-review/)[ ClaimA healthcare claim. ](/sdk/data-claim/)[ ClipboardFree-text content staged on a note by a clipboard command. ](/sdk/data-clipboard/)[ CommandStructured units of documentation in a patient's chart. ](/sdk/data-command/)[ CommandTypeSettingWhether each command type is turned on for the instance. ](/sdk/data-command-type-setting/)[ CommonEnumerationTypesCommon choice classes used in multiple models. ](/sdk/data-enumeration-types/)[ CompoundMedicationCompound medications, which are custom-made medications tailored to a patient's specific needs. ](/sdk/data-compound-medication/)[ ConditionCondition, diagnosis, or reason for seeking medical attention. ](/sdk/data-condition/)[ ContentTypeDjango content type ids used for generic relations and permalink generation. ](/sdk/data-content-type/)[ CoveragePatient insurance coverage. ](/sdk/data-coverage/)[ CreateCodingGapEventA record of a coding gap being created. ](/sdk/data-coding-gap-event/#createcodinggapevent)[ CustomCommandA plugin-defined read-only command recorded on a note (the custom command). ](/sdk/data-custom-command/)[ DeferCodingGapEventA record of a coding gap being deferred. ](/sdk/data-coding-gap-event/#defercodinggapevent)[ DetectedIssueActual or potential clinical issue with or between one or more active or proposed clinical actions for a patient. ](/sdk/data-detected-issue/)[ DeviceType of a manufactured item that is used in the provision of healthcare. ](/sdk/data-device/)[ DiagnosticViewA saved set of lab tests and questionnaire codes whose timeseries can be embedded in a note with the Reference command. ](/sdk/data-diagnostic-view/)[ DMEDurable medical equipment formulary products and the signed orders placed for them with the Order DME command. ](/sdk/data-dme/)[ DocumentReferenceReferences to documents stored in Canvas, with presigned URL support. ](/sdk/data-document-reference/)[ DocumentReviewDelegationA hand-off of a document review from one staff member (or team) to another, with signature consent. ](/sdk/data-document-review-delegation/)[ EducationalMaterialPatient educational material shared from a note via the Educational Material command. ](/sdk/data-educational-material/)[ EligibilityResponseA coverage eligibility (270/271) request and response, with the derived check status. ](/sdk/data-eligibility-response/)[ EligibilitySummarySummary of copay and coinsurance for a Coverage. ](/sdk/data-coverage/#eligibilitysummary)[ EncounterA patient Encounter connected to a Note in Canvas. ](/sdk/data-encounter/)[ ExternalEventExternal clinical events from ADT feeds such as admissions, discharges, and transfers. ](/sdk/data-external-event/)[ FacilityA location where healthcare services are provided. ](/sdk/data-facility/)[ FamilyHistoryA patient's family medical history — conditions recorded for a relative. ](/sdk/data-family-history/)[ FaxFaxes sent or received through Canvas, and the action events that record a document's fax delivery status. ](/sdk/data-fax/)[ FaxStatusModelThe status recorded when Canvas receives a fax. ](/sdk/data-fax/#faxstatusmodel)[ FollowUpA requested follow-up (recall) recorded on a note via the follow_up command. ](/sdk/data-follow-up/)[ GoalA goal for a patient. ](/sdk/data-goal/)[ Google CalendarProvider busy time and connection status from native Google Calendar sync. ](/sdk/data-google-calendar/)[ GroupA stable external identifier linking a Team or PatientGroup, as surfaced by the FHIR Group resource. ](/sdk/data-group/)[ HistoryOfPresentIllnessThe History of Present Illness (HPI) narrative recorded on a note. ](/sdk/data-history-present-illness/)[ ImagingAnalysis of imaging tests to obtain information about the health of a patient. ](/sdk/data-imaging/)[ ImagingOrderActionEventFaxes of an imaging order, with their delivery status. ](/sdk/data-fax/#imagingorderactionevent)[ ImagingReportTemplateTemplates used for imaging reports, defining fields and options. ](/sdk/data-imaging-report-template/)[ ImmunizationA record of a vaccination that is being administered to a patient, either now, in the past, or in the future. ](/sdk/data-immunization/)[ InstructionAn Instruct command committed in a patient's note — clinical guidance such as cessation counseling or dietary instructions. ](/sdk/data-instruction/)[ IntegrationTaskIncoming documents that need processing, including faxes, uploads, and portal submissions. ](/sdk/data-integration-task/)[ IntegrationTaskActionEventFaxes of an integration task, with their delivery status. ](/sdk/data-fax/#integrationtaskactionevent)[ InvoiceA patient statement generated for a patient or guarantor, with its total, delivery method, and status. ](/sdk/data-invoice/)[ LabOrderActionEventFaxes of a lab order, with their delivery status. ](/sdk/data-fax/#laborderactionevent)[ LabPartnerLab partners and the tests they offer within Canvas. ](/sdk/data-lab-partner-and-test/)[ LabReportTemplateTemplates for point-of-care labs and custom lab reports, defining fields and options. ](/sdk/data-lab-report-template/)[ LabsAnalysis of clinical specimens to obtain information about the health of a patient. ](/sdk/data-labs/)[ LetterPatient correspondence letters created within Canvas. ](/sdk/data-letter/)[ LetterActionEventActions taken on a letter, such as printing or faxing. ](/sdk/data-letter-action-event/)[ MedicationA record of a medication that is being consumed by a patient, either now, in the past, or in the future. ](/sdk/data-medication/)[ Medication HistoryA record of a patient's medication history, including medications that were taken in the past but are no longer active. ](/sdk/data-medication-history/)[ Medication StatementA record of a medication statement by a patient from the past. ](/sdk/data-medication-statement/)[ MessageMessages sent to and from Canvas. ](/sdk/data-message/)[ NoteClinical notes on patient charts. ](/sdk/data-note/)[ NoteActionEventFaxes of a note, with their delivery status. ](/sdk/data-fax/#noteactionevent)[ ObservationMeasurements and simple assertions made about a patient. ](/sdk/data-observation/)[ OrganizationThe clinical organization present in the Canvas EMR. ](/sdk/data-organization/)[ OrganizationalEntityExternal entities, such as service providers, referenced by a patient's external care team members. ](/sdk/data-organizational-entity/)[ PatientData used to categorize individuals for identification, records matching, and other purposes. ](/sdk/data-patient/)[ PatientAdministrativeDocumentPatient-facing administrative documents, such as signed consent forms and statements. ](/sdk/data-patient-administrative-document/)[ PatientConsentDocumented patient consents that ensure legal compliance and protect patient rights. ](/sdk/data-patient-consent/)[ PatientGroupA collection of patients. ](/sdk/data-patient-group/)[ PaymentCardA patient's stored payment card on file. ](/sdk/data-payment-card/)[ PayorSpecificChargeA billing charge specific to a certain transactor in Canvas. ](/sdk/data-payor-specific-charge/)[ PlanA Plan (plan of care) narrative recorded on a note. ](/sdk/data-plan/)[ PluginCommandCustom commands registered by plugins via the manifest configuration. ](/sdk/data-plugin-command/)[ PostingPayments and postings associated with healthcare claims. ](/sdk/data-posting/)[ PracticeLocationThe practice locations present in the Canvas EMR. ](/sdk/data-practicelocation/)[ PrescriptionThe practice locations present in the Canvas EMR. ](/sdk/data-prescription/)[ PrescriptionChangeRequestAn incoming Surescripts request to change a prescription, with its medication codings. ](/sdk/data-prescription-change-request/)[ PrescriptionChangeResponseA response (approve/deny) to a Surescripts prescription change request. ](/sdk/data-prescription-change-response/)[ ProcedureA procedure performed on or ordered for a patient, with its CPT codings. ](/sdk/data-procedure/)[ ProtocolCurrentThe current state of clinical protocols applied to patients within Canvas. ](/sdk/data-protocol-current/)[ ProtocolOverrideA record of a protocol being snoozed for a patient. ](/sdk/data-protocol-override/)[ QuestionnaireGroups of coded questions and the coded patient responses. ](/sdk/data-questionnaire/)[ ReasonForVisitThe reason for a patient's visit. ](/sdk/data-reason-for-visit/)[ ReceiptA Canvas-generated payment receipt for a payment collection. ](/sdk/data-receipt/)[ ReferenceThe record of a diagnostic view embedded in a note, with a snapshot of its rendered table. ](/sdk/data-reference/)[ ReferralA referral directing a specific patient to another provider or specialist. ](/sdk/data-referral/)[ ReferralActionEventFaxes of a referral, with their delivery status. ](/sdk/data-fax/#referralactionevent)[ RefillRequestAn incoming request to refill a patient's medication and its responding prescriptions. ](/sdk/data-refill-request/)[ RemoveAllergyEventA record of an allergy being removed via the remove_allergy command. ](/sdk/data-remove-allergy-event/)[ RemovePastMedicalHistoryEventA record of a past medical history entry being removed via the remove_past_medical_history command. ](/sdk/data-remove-past-medical-history-event/)[ ResolveConditionEventA record of a condition being resolved via the resolve_condition command. ](/sdk/data-resolve-condition-event/)[ ServiceProviderData associated with Service Providers. ](/sdk/data-serviceprovider/)[ SnapshotImages captured via the Canvas iOS application. ](/sdk/data-snapshot/)[ SpecialtyReportTemplateTemplates for specialty and referral reports, including specialty taxonomy codes. ](/sdk/data-specialty-report-template/)[ StaffData associated with Staff members. ](/sdk/data-staff/)[ Stop Medication EventA record of a Stop Medication Event, when a medication is removed from a patient's medication list. ](/sdk/data-stop-medication-event/)[ TaskData associated with Tasks. ](/sdk/data-task/)[ TeamData associated with Teams. ](/sdk/data-team/)[ Uncategorized Clinical DocumentsData associated with uncategorized clinical documents and their reviews. ](/sdk/data-uncategorized-clinical-document/)[ VaccineA vaccine in the instance's catalog and the lots of it held in inventory. ](/sdk/data-vaccine/)[ ValidateCodingGapEventA record of a coding gap being validated. ](/sdk/data-coding-gap-event/#validatecodinggapevent)[ ValueSetsLists of codes and terms from various clinical coding systems grouped by a defining concept. ](/sdk/data-value-sets/)[ VisualExamFindingVisual exam findings captured on a note — a titled image with a narrative description. ](/sdk/data-visual-exam-finding/)[ VitalSignReadingVital-sign readings recorded via the vitals command — the reading anchor and its individual measurements. ](/sdk/data-vital-sign-reading/) --- # Default Homepage Source: https://docs.canvasmedical.com/sdk/default-homepage-effect/ This allows developers to set a provider's default homepage in Canvas. The default homepage is the page that a provider sees when they log in to Canvas. This effect can be used to set the default homepage to a specific page or a plugin application. For more guidance, see "[How to set a default homepage for the provider application](/guides/set-default-homepage/)". Build a `DefaultHomepageEffect` with a `page` or an `application_identifier`, then return its `apply()` from a `GET_HOMEPAGE_CONFIGURATION` handler. ## Methods ### apply() → Effect Sets the default homepage for the user logging in. - Either `page` or `application_identifier` is required. If both are set, `application_identifier` takes precedence and the homepage is that application. - `application_identifier` must be the identifier of an installed application. When no plugin returns this effect, the default homepage is the schedule. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `page`| `Pages` `\| None`| The Canvas page to open.| One of `page` or `application_identifier` `application_identifier`| `str \| None`| The identifier of the plugin application to open.| One of `page` or `application_identifier` ## Pages An enumeration of pages that can be set as the default homepage, accessed as `DefaultHomepageEffect.Pages.`: Value --- `PATIENTS` `SCHEDULE` `REVENUE` `CAMPAIGNS` `DATA_INTEGRATION` ## Example ```python from canvas_sdk.effects.default_homepage import DefaultHomepageEffect DefaultHomepageEffect(page=DefaultHomepageEffect.Pages.PATIENTS).apply() ``` ```python from canvas_sdk.effects.default_homepage import DefaultHomepageEffect DefaultHomepageEffect(application_identifier="app_identifier").apply() ``` --- # Application Notification Badge Source: https://docs.canvasmedical.com/sdk/effect-application-notification-badge/ Notification badges let your plugin show a count on an [application](/sdk/handlers-applications/), such as how many unread items are waiting. Where the badge appears depends on the application's [scope](/sdk/handlers-applications/#application-scopes): Scope| Where the badge appears ---|--- `global`| On the application's icon in the app drawer. `patient_specific`| On the application's icon in the app drawer, or on the panel when the application sets `show_in_panel`. `provider_menu_item`| Next to the application's label in the provider menu. `portal_menu_item`| Next to the application's label in the patient portal menu, for the patient who is logged in. Applications in other scopes, such as `full_chart` and the Provider Companion scopes, do not show badges. There are two ways a badge is set: - **On load** — override `compute_notification_badge()` on your `Application` handler to provide the initial count shown when Canvas loads applications. See [Notification Badges](/sdk/handlers-applications/#notification-badges) on the Applications handler page. - **Live updates** — emit an `ApplicationNotificationBadge` effect from any event handler to update the count in real time, without the user reloading the page. This is what the rest of this page covers. ## Setting a badge `ApplicationNotificationBadge` is a fluent builder. Construct it with the target application's identifier, optionally call `.filter(...)` to target patients, then return `.broadcast(...)` from your handler. ### Methods #### filter(*, patient_ids: list[str] | None = None) → ApplicationNotificationBadge Scopes the update to the listed patients: their charts, and their own patient portal. Returns the builder, so the call chains into `.broadcast(...)`. #### broadcast(count: int, staff_ids: list[str] | None = None) → Effect Sets the badge to `count` for the staff and patients you target. See Targeting. - `count` is required and must be `>= 0`; a count of `0` clears the badge. - The `application_identifier` passed to the constructor must match an installed application, or the effect raises a validation error. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `application_identifier`| `str`| Passed to the constructor. Must match the application's `class` string declared in [`CANVAS_MANIFEST.json`](/sdk/canvas_manifest/#applications), the `:` value (identical to the handler's `identifier`).| Yes `count`| `int`| Passed to `.broadcast()`. The badge value to display.| Yes `staff_ids`| `list[str]`| Passed to `.broadcast()`. Ids of the [staff](/sdk/data-staff/) who should see the update.| No `patient_ids`| `list[str]`| Passed to `.filter()`. Ids of the [patients](/sdk/data-patient/) the update applies to: their charts, and their own patient portal.| No The `application_identifier` is the application's `class` string from `CANVAS_MANIFEST.json` (`:`). For example, an `InboxApp` defined in `my_plugin/apps/inbox.py` and registered like this: ```json "applications": [ { "class": "my_plugin.apps.inbox:InboxApp", "name": "Inbox", "description": "Unread items inbox", "icon": "/assets/inbox.png", "scope": "global" } ] ``` is targeted by that same `class` string: ```python from canvas_sdk.effects.application_notification_badge import ApplicationNotificationBadge # Set a badge of 3 for a specific staff member. ApplicationNotificationBadge("my_plugin.apps.inbox:InboxApp").broadcast(count=3, staff_ids=["staff-id"]) ``` ## Targeting `staff_ids` and `patient_ids` control who sees the update. An empty list means "all" on that axis: `staff_ids`| `patient_ids`| Who sees the badge ---|---|--- set| empty| The listed staff, on any patient (and on global views). empty| set| Staff currently viewing the listed patients' charts, and the listed patients in the patient portal. set| set| The listed staff, but only while viewing the listed patients' charts. empty| empty| All staff, all patients (a system-wide update). The patient portal is not updated. The patient portal receives an update only when you set `patient_ids` and leave `staff_ids` empty. Each listed patient sees the new count on the matching `portal_menu_item` application while they're logged in to the portal. Updates that name staff, and system-wide updates, never reach the portal. A patient only ever receives updates sent for their own id. Because `patient_ids` reaches both audiences, the application's scope decides where the badge appears. A `patient_specific` application shows it to staff on the chart. A `portal_menu_item` application shows it to the patient in the portal. > **Note on "all patients" (empty`patient_ids`):** the update is delivered **live** only to charts a staff member currently has open. Other patients' charts reflect the new value the next time they're loaded, via `compute_notification_badge()`. So for a badge that should read the same across every patient, have `compute_notification_badge()` return a patient-independent count (ignore the patient in `self.event.context`). A push then keeps the open chart live, and the load-time hook covers the rest. ```python from canvas_sdk.effects.application_notification_badge import ApplicationNotificationBadge # Show a badge to staff viewing a specific patient's chart. ApplicationNotificationBadge("my_plugin.apps.patient_labs:PatientLabsApp").filter( patient_ids=["patient-id"] ).broadcast(count=5) # Show a badge to a patient in the patient portal (portal_menu_item application). ApplicationNotificationBadge("my_plugin.apps.messages:PortalMessagesApp").filter( patient_ids=["patient-id"] ).broadcast(count=2) # Combine: only the on-call provider, and only on this patient's chart. ApplicationNotificationBadge("my_plugin.apps.patient_labs:PatientLabsApp").filter( patient_ids=["patient-id"] ).broadcast(count=1, staff_ids=["staff-id"]) ``` ## Reacting to events The most common pattern is updating a badge in response to a domain event. Here a handler recomputes a staff member's inbox count whenever a task is created and pushes the new value live: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.application_notification_badge import ApplicationNotificationBadge from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data.task import Task, TaskStatus class InboxBadgeHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.TASK__CREATED) def compute(self) -> list[Effect]: task = Task.objects.get(id=self.event.target.id) assignee = task.assignee if not assignee: return [] open_count = Task.objects.filter(assignee=assignee, status=TaskStatus.OPEN).count() return [ ApplicationNotificationBadge("my_plugin.apps.inbox:InboxApp").broadcast( count=open_count, staff_ids=[assignee.id], ) ] ``` ## Clearing a badge Broadcast a `count` of `0` to remove the badge from the icon: ```python from canvas_sdk.effects.application_notification_badge import ApplicationNotificationBadge ApplicationNotificationBadge("my_plugin.apps.inbox:InboxApp").broadcast(count=0, staff_ids=["staff-id"]) ``` > **Note:** To set the badge value shown when applications first load (rather than in response to an event), override `compute_notification_badge()` on your `Application` handler. See [Notification Badges](/sdk/handlers-applications/#notification-badges). --- # Appointment Labels Source: https://docs.canvasmedical.com/sdk/effect-appointment-labels/ # Appointment Label Effects The appointment label effects provide programmatic management of labels in Canvas. Labels serve as visual indicators and categorization tools, enabling automated workflows and improved organization for appointments. ## Overview Labels are a powerful way to categorize and track appointments. Canvas supports up to 3 labels per appointment, and these effects allow plugins to automatically manage labels based on business logic. ## AddAppointmentLabel Effect The `AddAppointmentLabel` effect adds one or more labels to an existing appointment. ### Methods #### apply() → Effect Adds the specified labels to the appointment. ##### Returns An `Effect` object configured for adding appointment labels. ##### Behavior - Labels are added to the appointment if the total count doesn't exceed 3 - Labels are automatically sorted for consistency - Duplicate labels are ignored (labels are stored as a set) - Validates the appointment exists before adding labels - Validates label names are non-empty strings - Returns an error if adding labels would exceed the 3-label limit ### Attributes Attribute| Type| Description| Required ---|---|---|--- `appointment_id`| `str`| ID of the appointment to add labels to| Yes `labels`| `set[str]`| Set of label names to add (1-3 labels total per appointment)| Yes ### Example ```python from canvas_sdk.effects.note.appointment import AddAppointmentLabel from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.APPOINTMENT_CREATED)] def compute(self): # Add labels to an appointment effect = AddAppointmentLabel( appointment_id="appointment-uuid", labels={"URGENT", "FOLLOW_UP"} ) return [effect.apply()] ``` If more than three labels are attempted to be added, a `ValidationError` will be raised. ```python from canvas_sdk.effects.note.appointment import AddAppointmentLabel from canvas_sdk.exceptions import ValidationError def handle_validation_errors(): # Example of handling validation errors try: effect = AddAppointmentLabel( appointment_id="invalid-id", labels={"LABEL1", "LABEL2", "LABEL3", "LABEL4"} # Would exceed limit ) return [effect.apply()] except ValidationError as e: # Handle validation errors return [] ``` * * * ## RemoveAppointmentLabel Effect The `RemoveAppointmentLabel` effect removes one or more labels from an existing appointment. ### Methods #### apply() → Effect Removes the specified labels from the appointment. ##### Returns An `Effect` object configured for removing appointment labels. ##### Behavior - Removes the specified labels from the appointment - Non-existent labels are ignored (no error thrown) - Validates the appointment exists before removing labels ### Attributes Attribute| Type| Description| Required ---|---|---|--- `appointment_id`| `str`| ID of the appointment to remove labels from| Yes `labels`| `set[str]`| Set of label names to remove| Yes ### Example ```python from canvas_sdk.effects.note.appointment import RemoveAppointmentLabel from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.APPOINTMENT_LABEL_REMOVED)] def compute(self): # Remove labels from an appointment effect = RemoveAppointmentLabel( appointment_id="appointment-uuid", labels={"CANCELLED", "RESCHEDULED"} ) return [effect.apply()] ``` * * * ## Implementation Details ### Label Constraints - **Maximum labels** : 3 labels per appointment (enforced by validation) - **Label format** : Labels are strings, automatically sorted for consistency - **Uniqueness** : Labels are stored as a set, preventing duplicates - **Case sensitivity** : Label names are case-sensitive ### Validation Messages The effects provide clear error messages for common issues: - `"Appointment {appointment_id} does not exist"` \- When appointment ID is invalid - `"Limit reached: Only 3 appointment labels allowed. Attempted to add {count} label(s) to appointment with {existing} existing label(s)."` \- When label limit would be exceeded These effects work seamlessly with appointment label events: - `APPOINTMENT_LABEL_ADDED` \- Fired when labels are added - `APPOINTMENT_LABEL_REMOVED` \- Fired when labels are removed For more information on these events, see [Appointment Events](/sdk/events/#appointments). ## Related Documentation - [Appointment Events](/sdk/events/#appointments) \- Event documentation - [Appointment Coverage Label Example](https://github.com/canvas-medical/canvas-plugins/tree/main/example-plugins/appointment_coverage_label) \- Real-world example plugin --- # AppointmentMetadata Effect Source: https://docs.canvasmedical.com/sdk/effect-appointment-metadata/ The `AppointmentMetadata` effect provides a flexible key-value storage system for appointment-specific data within the Canvas system. This effect enables the creation and updating of custom metadata entries associated with appointment records. This allows for extensible appointment information storage beyond standard scheduling fields. ## Overview Appointment metadata serves as a powerful extension mechanism for storing custom appointment-related information that doesn't fit within the standard appointment data model. It uses the `.upsert(value)` method to apply a value to the key attributed with the Metadata effect object. ## Methods ### upsert(value: str) → Effect Creates or updates a metadata entry for the specified appointment and key combination. #### Parameters Parameter| Type| Description| Required ---|---|---|--- `value`| `str`| The metadata value to store| Yes #### Returns An `Effect` object configured for upserting appointment metadata. #### Behavior - If a metadata entry with the specified key already exists for the appointment, it will be updated with the new value - If no entry exists, a new metadata entry will be created - The operation is idempotent - repeated calls with the same key and value will not create duplicate entries ## Attributes Attribute| Type| Description| Required ---|---|---|--- `appointment_id`| `str`| Id of the [Appointment](/sdk/data-appointment/) record to associate metadata with| Yes `key`| `str`| Unique identifier for the metadata entry within the appointment context| Yes ## Implementation Details ### Validation The effect performs comprehensive validation before execution: 1. **Appointment Existence Validation** : Verifies that the referenced appointment exists in the system - Queries the appointment database to confirm the `appointment_id` corresponds to an existing appointment record - Returns a descriptive error if the appointment is not found 1. **Field Validation** : Ensures all required fields are provided and properly formatted - Both `appointment_id` and `key` must be non-empty strings - The `value` parameter in the `upsert` method must be provided ### Data Structure The effect payload is structured as JSON with the following schema: ```json { "data": { "appointment_id": "appointment-id", "key": "metadata-key", "value": "metadata-value" } } ``` ## Example Usage ### Basic Usage ```python from canvas_sdk.effects.appointments_metadata.base import AppointmentsMetadata # Create a metadata entry for appointment state metadata = AppointmentsMetadata( appointment_id="550e8400e29b41d4a716446655440001", key="state" ) # Upsert the metadata value effect = metadata.upsert("CA") ``` ## Best Practices ### Key Naming Conventions 1. **Use Descriptive Names** : Choose keys that clearly indicate the purpose of the metadata - Good: `external_mrn`, `preferred_pharmacy_id`, `risk_score_diabetes` - Avoid: `data1`, `temp`, `misc` 1. **Namespace Your Keys** : When building integrations or modules, prefix keys to avoid collisions - Example: `integration_patient_id`, `module_diabetes_last_a1c_date` ### Value Storage 1. **String Serialization** : All values are stored as strings. For complex data types: ```python import json from canvas_sdk.effects.appointments_metadata.base import AppointmentsMetadata metadata = AppointmentsMetadata( appointment_id="550e8400e29b41d4a716446655440001", key="result" ) complex_data = {"scores": [85, 92, 78], "average": 85.0} metadata.upsert(json.dumps(complex_data)) ``` 2. **Boolean Values** : Store as "true" or "false" strings for consistency ```python from canvas_sdk.effects.appointments_metadata.base import AppointmentsMetadata consented = True metadata = AppointmentsMetadata( appointment_id="550e8400e29b41d4a716446655440001", key="boolean_value" ) metadata.upsert("true" if consented else "false") ``` ## Notes - Metadata entries are appointment-specific and isolated - the same key can have different values for different appointments - There is no built-in versioning; updating a key overwrites the previous value - The system does not enforce any schema on metadata values - validation is the responsibility of the implementing code --- # Banner Alerts Source: https://docs.canvasmedical.com/sdk/effect-banner-alerts/ Use banner alerts to show contextual information in a patient's chart. > **Warning:** A banner alert belongs to the plugin that added it. Disabling the plugin hides its banners, and [uninstalling](/sdk/canvas_cli/#canvas-uninstall) it deletes them. ## Adding a Banner Alert To add a banner alert, build an `AddBannerAlert` and return its `apply()` from your handler. ### Methods #### apply() → Effect Adds the banner alert. - `key`, `narrative`, `placement`, and `intent` are required, along with either `patient_id` or `patient_filter`. - `narrative` can be at most 90 characters. ### Attributes Attribute| Type| Description| Required ---|---|---|--- patient_id| String| The id of the [patient](/sdk/data-patient/) the alert should be associated with.| If `patient_filter` is not provided patient_filter| dict| A patient filter that applies the effect to every matching patient. For example, `{"active": True}` applies the effect to all active patients| If `patient_id` is not provided key| String| An identifier that categorizes the alert.| Yes narrative| String| The content of the alert. Maximum 90 characters.| Yes placement| list[Placement]| List of areas the alert should show.| Yes intent| Intent| Affects the styling of the alert.| Yes href| String| If given, the alert will appear as a link to this URL.| No ### Example ```python from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.effects.banner_alert import AddBannerAlert class MyHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.PATIENT_UPDATED) def compute(self): banner = AddBannerAlert( patient_id=self.target, key="test-alert", narrative="This is only a test.", placement=[ AddBannerAlert.Placement.CHART, AddBannerAlert.Placement.APPOINTMENT_CARD, AddBannerAlert.Placement.SCHEDULING_CARD, ], intent=AddBannerAlert.Intent.INFO, href="https://docs.canvasmedical.com", ) return [banner.apply()] ``` To apply the effect to all active patients when a plugin is created or updated, include the `PLUGIN_CREATED` and/or `PLUGIN_UPDATED` events in the `RESPONDS_TO` list. Additionally, `patient_filter` can be used (instead of `patient_id`) on the `AddBannerAlert` class. ```python from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.effects.banner_alert import AddBannerAlert class MyHandler(BaseHandler): RESPONDS_TO = [ EventType.Name(EventType.PATIENT_UPDATED), EventType.Name(EventType.PLUGIN_CREATED), EventType.Name(EventType.PLUGIN_UPDATED), ] def compute(self): banner = AddBannerAlert( key="test-alert", narrative="This is only a test.", placement=[ AddBannerAlert.Placement.CHART, AddBannerAlert.Placement.APPOINTMENT_CARD, AddBannerAlert.Placement.SCHEDULING_CARD, ], intent=AddBannerAlert.Intent.INFO, href="https://docs.canvasmedical.com", ) if self.event.type in [EventType.PLUGIN_CREATED, EventType.PLUGIN_UPDATED]: banner.patient_filter = {"active": True} else: banner.patient_id = self.target return [banner.apply()] ``` ### Placement This determines where the banner alert appears. #### `Placement.CHART` This will place the banner under the patient's name on their chart ![](/assets/images/sdk/banner-alerts/banner_alert_placement_chart.png) #### `Placement.TIMELINE` This will place the banner on the top of the patient's timeline of notes in their chart ![](/assets/images/sdk/banner-alerts/banner_alert_placement_timeline.png) #### `Placement.APPOINTMENT_CARD` This will appear when you click an appointment on the calendar view ![](/assets/images/sdk/banner-alerts/banner_alert_placement_appointment_card.png) #### `Placement.SCHEDULING_CARD` This will appear when you select a patient during the scheduling of an appointment on the calendar view ![](/assets/images/sdk/banner-alerts/banner_alert_placement_scheduling_card.png) #### `Placement.PROFILE` This will place the banner under the patient's name on their patient registration page ![](/assets/images/sdk/banner-alerts/banner_alert_placement_profile.png) ### Intent The type or severity of an alert. This will change the appearance of the banner alert. #### `Intent.INFO` ![](/assets/images/sdk/banner-alerts/banner_alert_intent_info.png) #### `Intent.WARNING` ![](/assets/images/sdk/banner-alerts/banner_alert_intent_warning.png) #### `Intent.ALERT` ![](/assets/images/sdk/banner-alerts/banner_alert_intent_alert.png) ## Removing a Banner Alert To remove a banner alert, build a `RemoveBannerAlert` identifying the alert's key and patient, and return its `apply()` from your handler. ### Methods #### apply() → Effect Removes the banner alert. - `key` is required, along with either `patient_id` or `patient_filter`. ### Attributes Attribute| Type| Description| Required ---|---|---|--- patient_id| String| The id of the [patient](/sdk/data-patient/) whose alert should be removed.| If `patient_filter` is not provided patient_filter| dict| A patient filter that removes the alert from every matching patient.| If `patient_id` is not provided key| String| The key of the alert to remove.| Yes ### Example ```python from canvas_sdk.effects.banner_alert import RemoveBannerAlert banner_alert = RemoveBannerAlert( key='test-alert', patient_id="d4c933fe8f6948f6a7d2a42a2641b13b", ) banner_alert.apply() ``` ## Patient filters A `patient_filter` selects patients by patient field. Each key is a field name, optionally followed by `__` and a lookup, such as `"last_name__istartswith": "Sm"`. These fields are supported: - `id`, `key`, and `mrn` - `active` and `deceased` - `first_name`, `middle_name`, `last_name`, and `nickname` - `birth_date` and `sex_at_birth` - `external_identifiers__system` and `external_identifiers__value` These lookups are supported: `exact`, `iexact`, `in`, `lt`, `lte`, `gt`, `gte`, `range`, `isnull`, `startswith`, `istartswith`, `endswith`, `iendswith`, `contains`, and `icontains`. Each value is a string, number, boolean, `None`, or a list of strings, numbers, or booleans. --- # Batch Originate Commands Source: https://docs.canvasmedical.com/sdk/effect-batch-originate/ ## Overview The `BatchOriginateCommandEffect` provides an efficient way to insert multiple commands into a note simultaneously. When you need to create many commands at once, using batch originate significantly improves performance compared to individual originate operations. Build a `BatchOriginateCommandEffect` with the commands to insert, then return its `apply()` from your handler. ## Methods ### apply() → Effect Originates every command in `commands` in a single operation. - `commands` is required, and each command needs the fields its own `originate()` requires. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `commands`| `list`| List of command instances to batch originate.| Yes `line_number`| `int`| Which note line the commands land on. Defaults to `-1`, which inserts them at the bottom of the note; set a specific line to target that line instead. Combine with `replace_line=True` to also take over (replace the content of) that line.| No `replace_line`| `bool`| Replace the content of the target line (the one set by `line_number`) with the originated commands, instead of inserting them as new lines. Defaults to `False`.| No ## How It Works The batch originate effect processes multiple commands in a single operation: 1. **Command Preparation** : Each command in the list requires all necessary fields for `originate` 2. **Note Update** : The note is updated once with all command UUIDs, rather than updating for each command individually This approach minimizes database round-trips and improves overall performance. ## Commit behavior `BatchOriginateCommandEffect` originates commands in the **uncommitted (draft)** state only. The batch effect has no `commit` option — every command in the batch is inserted into the note body as a draft. Batch originating commands in a committed state is **not supported** , by design. The performance benefit of batching comes from collapsing the note update for many draft insertions into a single operation, and committing is a separate, per-command action with no equivalent batch saving. Whenever a plugin needs to originate more than one command — whether you want them left as drafts or committed — batch origination is the right tool. To end up with committed commands, batch originate the drafts first so the note is updated once, then commit each command individually. Assign each command a `command_uuid` up front so it can be committed after it is originated: ```python from uuid import uuid4 # Set command_uuid so each draft can be committed after batch origination plan1.command_uuid = str(uuid4()) assess.command_uuid = str(uuid4()) # One note update for all drafts, followed by a commit per command return [ BatchOriginateCommandEffect(commands=[plan1, assess]).apply(), plan1.commit(), assess.commit(), ] ``` For three commands this performs three originates, **one** note update, and three commits. Collapsing the draft insertions into a single note update is where the performance benefit comes from. ## Note body automations A [note body automation](/sdk/handlers-action-buttons/) is an entry a plugin adds to the note body's "/" command list. When a clinician selects the entry, the automation's `handle()` returns a `BatchOriginateCommandEffect` with `replace_line=True`. In this flow Canvas's note body "/" handling supplies the trigger-line position, so Canvas places the originated commands on the line the clinician typed the trigger on and replaces that line, rather than appending them to the note. The automation doesn't set `line_number` itself. If a plugin omits `replace_line`, it keeps its default of `False`, and the batch follows the effect's normal defaults: the originated commands insert at the bottom of the note (the `line_number=-1` default) rather than taking over the trigger line. ```python return [ BatchOriginateCommandEffect( commands=[plan], replace_line=True, ).apply() ] ``` ## Example ```python from canvas_sdk.commands import ( PlanCommand, HistoryOfPresentIllnessCommand, QuestionnaireCommand, AssessCommand ) from canvas_sdk.effects.batch_originate import BatchOriginateCommandEffect from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data import Questionnaire, Note from canvas_sdk.events import EventType class Handler(BaseHandler): def compute(self): note_uuid = str(Note.objects.last().id) # Create multiple commands plan1 = PlanCommand() plan1.narrative = "Order labs for lipid panel" plan1.note_uuid = note_uuid plan2 = PlanCommand() plan2.narrative = "Schedule follow-up in 3 months" plan2.note_uuid = note_uuid hpi = HistoryOfPresentIllnessCommand() hpi.narrative = "Annual wellness visit" hpi.note_uuid = note_uuid assess = AssessCommand() assess.icd10_code = "E11.9" assess.note_uuid = note_uuid assess.background = "Type 2 diabetes mellitus" # Add a questionnaire questionnaire = QuestionnaireCommand() questionnaire.note_uuid = note_uuid questionnaire_id = Questionnaire.objects.filter( name="Patient Health Questionnaire" ).first() if questionnaire_id: questionnaire.questionnaire_id = str(questionnaire_id.id) # Batch originate all commands commands_to_originate = [plan1, plan2, hpi, assess, questionnaire] return [BatchOriginateCommandEffect(commands=commands_to_originate).apply()] ``` ## Related Documentation - [Commands Overview](/sdk/commands) --- # Billing Line Items Source: https://docs.canvasmedical.com/sdk/effect-billing-line-items/ The Canvas SDK allows you to create, update, and remove Billing Line Items from the footer of a note. ## Adding a Billing Line Item To add a billing line item to a note, build an `AddBillingLineItem` and return its `apply()` from your handler. ### Methods #### apply() → Effect Adds the billing line item to the note's footer. - `note_id` and `cpt` are required. - `units` defaults to `1`. ### Attributes Attribute| Type| Description| Required ---|---|---|--- note_id| String| The id of the [Note](/sdk/data-note/) where the line item should be associated.| Yes cpt| String| The billing code to use for the line item.| Yes units| Integer| The number of units to bill for the code. Defaults to `1` if not provided.| No assessment_ids| list[String]| List of Assessment ids from the note that are relevant to the code, also referred to as "diagnosis pointers".| No modifiers| list[Coding]| The modifiers to create with the billing code.| No ### Example ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data import Command, Assessment from canvas_sdk.effects.billing_line_item import AddBillingLineItem class MyHandler(BaseHandler): RESPONDS_TO = [ EventType.Name(EventType.PERFORM_COMMAND__POST_ORIGINATE) ] def compute(self) -> list[Effect]: command_id = self.target command = Command.objects.get(id=command_id) note = command.note assessments = [ str(i) for i in Assessment.objects.filter(note_id=note.dbid).values_list( "id", flat=True ) ] b = AddBillingLineItem( note_id=str(note.id), cpt="99213", units=1, assessment_ids=assessments, modifiers=[ {"code": "25", "system": "http://www.ama-assn.org/go/cpt"}, {"code": "59", "system": "http://www.ama-assn.org/go/cpt"}, ], ) return [b.apply()] ``` You don't set the line item's description in your plugin. When the line item is created, Canvas matches the `cpt` to a [ChargeDescriptionMaster](/sdk/data-charge-description-master/) charge and populates the description from that charge's `short_name`, truncated to 255 characters. If no charge matches the `cpt`, the description is left empty. ### Adding billing codes from an external system To populate diagnosis and CPT codes in a note footer from an external billing system, expose a [SimpleAPI](/sdk/handlers-simple-api-http/) endpoint that the system can call. The endpoint receives the note, CPT code, units, and ICD-10 codes, matches the ICD-10 codes to the note's assessments, and returns an `AddBillingLineItem` effect whose `assessment_ids` act as the line item's diagnosis pointers. This example authenticates with [`APIKeyAuthMixin`](/sdk/handlers-simple-api-http/#authentication-mixins), which checks requests against a plugin secret named `simpleapi-api-key`: ```python from http import HTTPStatus from canvas_sdk.effects import Effect from canvas_sdk.effects.billing_line_item import AddBillingLineItem from canvas_sdk.effects.simple_api import JSONResponse, Response from canvas_sdk.handlers.simple_api import APIKeyAuthMixin, SimpleAPIRoute from canvas_sdk.v1.data.assessment import Assessment class BillingAPI(APIKeyAuthMixin, SimpleAPIRoute): PATH = "/billing/add-line-item" def post(self) -> list[Response | Effect]: # {"note_id": "...", "cpt_code": "99213", "units": 1, "icd10_codes": ["E119", "I10"]} body = self.request.json() note_id = body["note_id"] icd10_codes = {code.upper().replace(".", "") for code in body.get("icd10_codes", [])} # Use each of the note's assessments whose condition carries a requested ICD-10 code assessment_ids = [ str(assessment.id) for assessment in Assessment.objects.filter(note__id=note_id).select_related("condition") if assessment.condition and any( coding.system == "ICD-10" and coding.code.upper().replace(".", "") in icd10_codes for coding in assessment.condition.codings.all() ) ] effect = AddBillingLineItem( note_id=note_id, cpt=body["cpt_code"], units=body.get("units", 1), assessment_ids=assessment_ids, ) return [ effect.apply(), JSONResponse( {"message": "Billing line item sent to Canvas", "assessment_ids": assessment_ids}, status_code=HTTPStatus.CREATED, ), ] ``` The [`cpt-billing-api` example plugin](https://github.com/Medical-Software-Foundation/canvas/tree/main/extensions/cpt-billing-api/cpt_billing_api) builds on this with request validation, a check that the CPT code is active in the charge description master, and a response listing which ICD-10 codes matched. > **Warning:** The billing line item is added to the note footer, not directly to a claim. As with any billing change, it must be applied before the note is locked, because locking pushes the note's charges to the claim. To apply it to the claim right away instead of at lock time, follow it with the Note effect's [`push_charges()`](/sdk/effect-notes/#push-charges) on the same note. If the API call succeeds but nothing appears on the note, confirm the note is unlocked and that the `note_id` resolves to the intended note. ## Updating a Billing Line Item To update a billing line item, build an `UpdateBillingLineItem` and return its `apply()` from your handler. ### Methods #### apply() → Effect Updates the billing line item. - `billing_line_item_id` is required. - Only the attributes you set on the effect are changed. Attributes you leave unset keep their current values. ### Attributes Attribute| Type| Description| Required ---|---|---|--- billing_line_item_id| String| The id of the [BillingLineItem](/sdk/data-billing-line-item/) to update.| Yes cpt| String| The billing code to use for the line item.| No units| Integer| The number of units to bill for the code.| No assessment_ids| list[String]| List of Assessment ids from the note that are relevant to the code, also referred to as "diagnosis pointers".| No modifiers| list[Coding]| The modifiers to create with the billing code.| No ### Example ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data import Assessment, Command, BillingLineItem from canvas_sdk.effects.billing_line_item import UpdateBillingLineItem class MyHandler(BaseHandler): RESPONDS_TO = [ EventType.Name(EventType.PERFORM_COMMAND__POST_COMMIT) ] def compute(self) -> list[Effect]: command_id = self.target command = Command.objects.get(id=command_id) note = command.note cpt = command.data["perform"]["value"] b_ids = BillingLineItem.objects.filter(cpt="99213", note=note).values_list( "id", flat=True ) assessment = Assessment.objects.filter(note_id=note.dbid).first() updates = [ UpdateBillingLineItem( billing_line_item_id=str(b_id), cpt=cpt, units=1, assessment_ids=[str(assessment.id)], modifiers=[{"code": "47", "system": "http://www.ama-assn.org/go/cpt"}], ) for b_id in b_ids ] return [update.apply() for update in updates] ``` ## Removing a Billing Line Item To remove a billing line item, build a `RemoveBillingLineItem` and return its `apply()` from your handler. When a Perform command is entered in error, Canvas removes the billing line item that the command added to the note footer. You don't need a plugin to remove it. ### Methods #### apply() → Effect Removes the billing line item from the note's footer. - `billing_line_item_id` is required. ### Attributes Attribute| Type| Description| Required ---|---|---|--- billing_line_item_id| String| The id of the [BillingLineItem](/sdk/data-billing-line-item/) to remove.| Yes ### Example When a Perform command is entered in error, this handler removes every billing line item on the note that has the command's CPT code. That includes matching line items that came from other sources, such as another plugin, which Canvas doesn't remove on its own. ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data import Command, BillingLineItem from canvas_sdk.effects.billing_line_item import RemoveBillingLineItem class MyHandler(BaseHandler): RESPONDS_TO = [ EventType.Name(EventType.PERFORM_COMMAND__POST_ENTER_IN_ERROR) ] def compute(self) -> list[Effect]: command_id = self.target command = Command.objects.get(id=command_id) cpt = command.data["perform"]["value"] note_id = command.note.dbid b_ids = BillingLineItem.objects.filter(cpt=cpt, note_id=note_id).values_list( "id", flat=True ) return [ RemoveBillingLineItem(billing_line_item_id=str(b_id)).apply() for b_id in b_ids ] ``` For more information about the BillingLineItem data class, check out [this page](/sdk/data-billing-line-item). --- # Charge Stored Card Source: https://docs.canvasmedical.com/sdk/effect-charge-stored-card/ The `ChargeStoredCard` effect charges a patient's stored payment card through the payment processor configured for your Canvas instance. The charge is processed server-side against the tokenized card, so no card data crosses the plugin boundary. You reference the card by an identifier the configured processor understands, never the underlying processor token: - The built-in Stripe processor takes the Canvas [PaymentCard](/sdk/data-payment-card/) id. - A [custom payment processor](/sdk/handlers-payment-processors/) takes whichever reference that processor manages. > **Warning:** **You are responsible for obtaining patient consent before charging a stored card.** Canvas does not enforce consent for charges initiated through this effect. ## Imports ```python from canvas_sdk.effects.payment import ChargeStoredCard ``` ## Charging a stored card Build a `ChargeStoredCard` and return its `apply()` from your handler. ### Methods #### apply() → Effect Charges the stored card. - `patient_id`, `payment_card_id`, `amount`, and `idempotency_key` are required, and `patient_id` must be the id of an existing patient. - `claim_id` is required when `copay` is `true`, and must be the id of an existing claim when set. ### Attributes Attribute| Type| Description| Required ---|---|---|--- patient_id| String| The Canvas [Patient](/sdk/data-patient/#patient) id to charge.| Yes payment_card_id| String| A reference to the stored card that the configured processor resolves: the Canvas [PaymentCard](/sdk/data-payment-card/) id for the built-in Stripe processor, or a custom processor's own reference.| Yes amount| Decimal| The amount to charge, in dollars, with up to two decimal places (for example, `Decimal("49.99")`). Must be greater than `0`.| Yes idempotency_key| UUID or String| A key that makes the charge safe to retry. Reusing the same key for a retry guarantees the patient is not charged twice. See Idempotency.| Yes claim_id| UUID or String| A [Claim](/sdk/data-claim/) id to post the payment against. When omitted, the payment is allocated across the patient's outstanding balance. See Payment allocation.| When `copay` is `true` copay| Boolean| Whether the charge is a copay. When `true`, the amount is posted to the claim's copay line item; requires `claim_id`. Defaults to `false`. See Payment allocation.| No description| String| A free-text description to record with the payment. It has no length limit, and is recorded only on a charge that names a `claim_id`.| No ### Example ```python from decimal import Decimal from uuid import uuid5, NAMESPACE_URL from canvas_sdk.effects import Effect from canvas_sdk.effects.payment import ChargeStoredCard from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data import Patient class ChargeBalanceOnBooking(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.APPOINTMENT_CREATED)] def compute(self) -> list[Effect]: # APPOINTMENT_CREATED targets the appointment; the patient is in the context. appointment_id = self.event.target.id patient = Patient.objects.get(id=self.event.context["patient"]["id"]) card = patient.payment_cards.filter(is_default=True).first() if card is None: return [] charge = ChargeStoredCard( patient_id=str(patient.id), payment_card_id=str(card.id), amount=Decimal("25.00"), idempotency_key=uuid5(NAMESPACE_URL, f"appointment-{appointment_id}-balance"), ) return [charge.apply()] ``` This charge names no `claim_id`, so it pays down the patient's outstanding balance and is rejected unless that balance covers the amount. To take a copay instead, supply the `claim_id` it belongs to and set `copay=True`. See Payment allocation. ### Validation `.apply()` raises a `ValidationError` when: - The `patient_id` does not match a patient. - A supplied `claim_id` does not match a claim. - `copay` is set without a `claim_id`. The card itself is not checked here. `payment_card_id` is resolved and validated server-side when the charge is processed, so a reference the configured processor cannot resolve arrives as an error on the response event rather than raising from `.apply()`. See Error codes for the code each processor reports. ### Card ownership Under the built-in Stripe processor, Canvas looks the card up scoped to `patient_id`, so a card belonging to a different patient, or one that has been removed, is rejected as `card_not_found` before the card is charged. A [custom payment processor](/sdk/handlers-payment-processors/) keeps its own cards, which Canvas cannot see, so it passes `payment_card_id` through without checking it. On a custom processor, confirming that a card reference belongs to the patient being charged is the processor's responsibility. ## Idempotency Every charge requires an `idempotency_key`. Pass either a `UUID` object or its string form, dashed or not; a string that does not parse as a UUID is rejected. Reusing the same key on a retry guarantees the patient is not charged twice, so the key must be **stable across retries of the same logical charge**. Generate it deterministically from a stable identifier — for example `uuid5(namespace, f"appointment-{id}-charge")` — or persist a `uuid4` before emitting the effect. Do not generate a fresh key on each attempt. ## Payment allocation The payment is always applied to the patient's account: - **Outstanding balance (default).** When no `claim_id` is given, the payment is allocated across the patient's outstanding balance. This is the only case where the amount is measured against a balance: the charge is rejected before the card is touched if the patient has no outstanding balance, and again if the amount exceeds it, so every charged dollar lands on a claim. - **Against a specific claim.** When `claim_id` is given and `copay` is left `false` (the default), the payment pays down that claim's non-copay line-item balances. The charge is rejected if the claim has nothing to charge against, but the amount is not capped at the claim's balance, so an amount above it leaves the claim in credit. - **As a copay on a claim.** When `claim_id` is given with `copay` set to `true`, the amount is recorded as a copay on that claim, posted to its copay line item. No balance is checked at all, so use this to collect a copay or prepayment against a claim that carries nothing to charge. ## Reconciling the charge Once the charge has been processed, Canvas emits a [`REVENUE__STORED_CARD__CHARGE_RESPONSE`](/sdk/events/#stored-card-charge-events) event carrying the outcome. It fires whether or not the card was charged, and nothing a handler returns changes the charge, so handle it to reconcile rather than to intervene. The event targets the patient that was charged: `self.event.target.id` is the patient id, and `self.event.target.instance` is the [Patient](/sdk/data-patient/#patient). Its actor is the one that emitted the originating effect. Correlate each response with the charge that produced it using the `idempotency_key` you supplied on the request. It is the only field that ties the two together, since nothing in the response identifies the handler that asked for the charge. This handler records the result of each charge, reading `payment_intent_id` when it succeeds and the `error` object when it does not: ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from logger import log class ReconcileStoredCardCharge(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.REVENUE__STORED_CARD__CHARGE_RESPONSE)] def compute(self) -> list[Effect]: context = self.event.context idempotency_key = context["idempotency_key"] if context["success"]: payment_intent_id = context["payment_intent_id"] log.info(f"Charge {idempotency_key} succeeded: {payment_intent_id}") else: error = context["error"] log.error(f"Charge {idempotency_key} failed [{error['code']}]: {error['message']}") return [] ``` ### Response context `self.event.context` arrives already parsed as a dictionary, so read the fields from it directly. It carries no card data or PHI. Apart from `success`, which is a boolean, and `error`, which is an object, every value is a string or `null`, including the amount and the ids that were UUIDs on the request, so cast `amount` before doing arithmetic on it. Field| Type| Description ---|---|--- success| Boolean| Whether the charge succeeded. payment_intent_id| String or null| The processor's payment identifier on success — the Stripe PaymentIntent id, or a custom processor's transaction id. `null` on failure. error| Object or null| `null` on success. On failure, `error` is an object with `code` and `message` fields. See Error codes. idempotency_key| String| Echoed from the request. Correlate the response with the originating charge using this value. patient_id| String| Echoed from the request: the [Patient](/sdk/data-patient/#patient) that was charged. payment_card_id| String| Echoed from the request: the stored card that was charged, a [PaymentCard](/sdk/data-payment-card/) id under the built-in processor. claim_id| String or null| Echoed from the request: the [Claim](/sdk/data-claim/) the payment was posted against, or `null` when none was supplied. amount| String| Echoed from the request: the dollar amount that was submitted. ### Error codes On failure, `error.code` names the condition that stopped the charge. `error.code`| When it occurs ---|--- `patient_not_found`| The `patient_id` did not match a patient. `card_not_found`| The stored card was not found. For the built-in Stripe processor, this also covers a card that exists but is not owned by the patient; a custom processor may raise a different code for that case — check the processor's own documentation. `claim_not_found`| The `claim_id` did not match a claim owned by the patient. `no_outstanding_balance`| A charge with no `claim_id` was attempted, but the patient has no outstanding balance. The balance is checked before the card is charged. `amount_exceeds_balance`| A charge with no `claim_id` exceeds the patient's outstanding balance. The amount is validated against the balance before the card is charged. `nothing_to_charge`| A per-claim charge with `copay` set to `false` was attempted, but the claim has no chargeable, non-copay balance. Set `copay` to `true` to record a copay or prepayment instead. `charge_failed`| An unexpected failure occurred while processing the charge. Retry using the same `idempotency_key`; the retry will not double-charge the patient. The payment processor's own decline codes also pass through in `error.code` — for example, `card_declined` from Stripe — and can arrive at charge time. Handle an unrecognized code gracefully rather than matching against a fixed set, since the processor can surface codes this list does not name. --- # Claim Effects Source: https://docs.canvasmedical.com/sdk/effect-claims/ The Canvas SDK provides effects to facilitate managing claims. The `ClaimEffect` class provides a unified interface for: - adding labels to claims - removing labels from claims - moving claim to a queue - adding comments to claims - posting payments to claims - upserting metadata on claims - adding banner alerts to claims - removing banner alerts from claims - updating provider information on claims - updating the supervising provider on claims - setting the incident-to flag on claims Additionally, the SDK provides a separate effect to update claim line items. The following standalone effect classes are deprecated and will be removed in a future release. Please use the `ClaimEffect` class instead. Deprecated Class| Old Import Path| New Equivalent ---|---|--- `AddClaimLabel`| `canvas_sdk.effects.claim_label`| `ClaimEffect.add_labels()` `RemoveClaimLabel`| `canvas_sdk.effects.claim_label`| `ClaimEffect.remove_labels()` `MoveClaimToQueue`| `canvas_sdk.effects.claim_queue`| `ClaimEffect.move_to_queue()` `AddClaimComment`| `canvas_sdk.effects.claim_comment`| `ClaimEffect.add_comment()` `PostClaimPayment`| `canvas_sdk.effects.payment`| `ClaimEffect.post_payment()` ## Claim Effect The `ClaimEffect` class facilitates operations on existing claims. `from canvas_sdk.effects.claim import ClaimEffect` Build a `ClaimEffect` with the claim's id, then return one of the methods below from your handler. Each method takes its own parameters and returns an `Effect`. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `claim_id`| `UUID` or `str`| Identifier for the claim| Yes ### Add Labels `add_labels(labels: list[str | Label]) → Effect` Adds one or more labels to a claim, and optionally creates new labels before assigning them to the claim. #### Parameters Parameter| Type| Description| Required ---|---|---|--- `labels`| `list[str or Label]`| List of label names and Label dataclasses* to apply to the claim| Yes *Labels can be passed in by name or as a Label dataclass. If the label with the provided name or values does not exist in your Canvas instance, it will be created and then applied to the specified claim. However, if a label already exists with the provided name or properties, it will add this existing label to the claim. #### Label The `Label` dataclass represents a label with specific properties, including color and name. Attribute| Type| Description| Required ---|---|---|--- `color`| [ColorEnum](/sdk/data-enumeration-types/#colorenum)| The color of the label in the UI| Yes `name`| `str`| The display name of the label| Yes #### Implementation Details - Validates `claim_id` is provided and that the associated claim exists. - Validates that `labels` are provided and non-empty. #### Example Usage ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.effects.claim import ClaimEffect, Label from canvas_sdk.v1.data import Note from canvas_sdk.v1.data.common import ColorEnum class MyHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED) def compute(self) -> list[Effect]: """Creates and adds a new label the claim when charges are pushed. Adds the existing Urgent label when the note is locked.""" note = Note.objects.get(id=self.event.context["note_id"]) claim = note.get_claim() state = self.event.context["state"] if state == "PSH": claim_effect = ClaimEffect(claim_id=claim.id) return [claim_effect.add_labels([Label(color=ColorEnum.PINK, name="pushed not locked")])] elif state == "LKD": claim_effect = ClaimEffect(claim_id=claim.id) return [claim_effect.add_labels(["Urgent"])] return [] ``` ### Remove Labels `remove_labels(labels: list[str]) → Effect` Removes existing labels from a claim. #### Parameters Parameter| Type| Description| Required ---|---|---|--- `labels`| `list[str]`| List of label names to remove from the claim| Yes #### Implementation Details - Validates `claim_id` is provided and that the associated claim exists - Validates `labels` is provided and non-empty #### Example Usage ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.effects.claim import ClaimEffect from canvas_sdk.v1.data import Note class MyHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED) def compute(self) -> list[Effect]: """When note is locked, remove the 'pushed not locked' label from the claim.""" note = Note.objects.get(id=self.event.context["note_id"]) claim = note.get_claim() state = self.event.context["state"] if state == "LKD": claim_effect = ClaimEffect(claim_id=claim.id) return [claim_effect.remove_labels(["pushed not locked"])] return [] ``` ### Move to Queue `move_to_queue(queue: str) → Effect` Moves a claim to a specific queue. Combined with the [claim data module](/sdk/data-claim/) and the label effects above, this effect lets a plugin control which claims land in the coding queue. For example: - When a visit does not meet your billable criteria, move its claim out of the coding queue so no one has to clean it up by hand. - Add labels to categorize claims for downstream workflows. #### Parameters Parameter| Type| Description| Required ---|---|---|--- `queue`| `str`| The name of the queue to move the claim to, which must be a [valid name](/sdk/data-claim/#claimqueues)| Yes #### Implementation Details - Validates `claim_id` is provided and that the associated claim exists - Validates `queue` is provided and the [queue with that name exists](/sdk/data-claim/#claimqueues) #### Example Usage ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.effects.claim import ClaimEffect from canvas_sdk.v1.data import Note class MyHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED) def compute(self) -> list[Effect]: if self.event.context["state"] == "ULK": note = Note.objects.get(id=self.event.context["note_id"]) claim = note.get_claim() claim_effect = ClaimEffect(claim_id=str(claim.id)) return [claim_effect.move_to_queue("NeedsClinicianReview")] return [] ``` ### Add Comment `add_comment(comment: str) → Effect` Creates a new comment on a claim. #### Parameters Parameter| Type| Description| Required ---|---|---|--- `comment`| `str`| The comment text to add| Yes #### Implementation Details - Validates `claim_id` is provided and that the associated claim exists #### Example Usage ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.effects.claim import ClaimEffect from canvas_sdk.v1.data import Patient, Claim class MyHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.COVERAGE_CREATED) def compute(self) -> list[Effect]: pt = Patient.objects.get(id=self.event.context["patient"]["id"]) # patient's claims that have not been submitted yet pt_claims = Claim.objects.filter( note__patient=pt, current_queue__queue_sort_ordering__in=[1, 2, 3, 4] ) return [ ClaimEffect(claim_id=claim.id).add_comment( "Patient has a new coverage, please confirm if this claim's coverage info should be updated." ) for claim in pt_claims ] ``` ### Post Payment `post_payment(claim_coverage_id, line_item_transactions, method, ...) → Effect` Posts a payment to a claim, specifying payment details and line item transactions. This method supports payments from insurance or patient and allows you to specify payments, adjustments, transfers, and write-offs on individual claim line items. #### Parameters Parameter| Type| Description| Required ---|---|---|--- `claim_coverage_id`| `UUID`, `str`, or `'patient'`| Identifier for the coverage or the string `'patient'` for patient payments.| Yes `line_item_transactions`| `list[LineItemTransaction]`| List of LineItemTransactions for claim line items.| Yes `method`| `PaymentMethod`| The PaymentMethod used (e.g., `cash`, `check`, `card`, `other`).| Yes `move_to_queue_name`| `str`| Name of the queue to move the claim to after payment.| No `claim_description`| `str`| Description for the claim allocation.| No `check_date`| `date`| Date of the check (required if method is `check`).| No `check_number`| `str`| Check number (required if method is `check`).| No `deposit_date`| `date`| Date the payment was deposited.| No `payment_description`| `str`| Description of the payment.| No #### Validations and Implementation Details - `check_number` and `check_date` are required if payment method is `check` - `claim_id` must correspond to a valid existing claim. For insurance payments, there are a few ways to help you identify the correct claim using the [Claim](/sdk/data-claim/#claim), [ClaimSubmission](/sdk/data-claim/#claimsubmission), [ClaimCoverage](/sdk/data-claim/#claimcoverage) data models: - `Claim.account_number` is the identifier that Canvas sends to the clearinghouse as a unique Canvas identifier for the claim. - `ClaimSubmission.clearinghouse_claim_id` is the identifier that the clearinghouse sends back to Canvas after they have accepted the claim, and is used for the clearinghouse's internal tracking of the claim. - `ClaimCoverage.payer_icn` is the identifier that the insurance company uses for their internal tracking of the claim, and is usually provided to Canvas via the clearinghouse. - `claim_coverage_id` must be either the string `"patient"` or correspond to a valid and **active** [ClaimCoverage](/sdk/data-claim/#claimcoverage) for the Claim. - A helpful way to identify the correct claim coverage is to use the method `get_coverage_by_payer_id(payer_id: str, subscriber_number: str | None = None)` on the [Claim](/sdk/data-claim/#claim) data model, where `payer_id` is the standard id for the insurance company. You can optionally provide `subscriber_number` if it's possible that the patient has multiple coverages from the same payer and you want to identify the correct coverage. - `move_to_queue_name` must be a valid label from [ClaimQueue](/sdk/data-claim/#claimqueues), but is not required. If provided, the claim will move to this queue after payment is applied. - The payment and its line item transactions are recorded together. If any part of the posting fails, none of it is saved. After the posting is saved, Canvas sends one [CLAIM_BALANCE_CHANGED](/sdk/events/#claims) event for the claim with its final balances. #### LineItemTransaction Attribute| Type| Description| Required ---|---|---|--- `claim_line_item_id`| `UUID` or `str`| Identifier for the claim line item.| Yes `charged`| `Decimal`| Charged amount for the line item.| No `allowed`| `Decimal`| Allowed amount for the line item.| No `payment`| `Decimal`| Payment amount for the line item.| No `adjustment`| `Decimal`| Adjustment amount for the line item.| No `adjustment_code`| `str`| Code describing the adjustment.| No `transfer_remaining_balance_to`| `UUID`, `str`, or `'patient'`| Transfer remaining balance to another payer or patient.| No `write_off`| `bool`| Whether to write off the remaining balance.| No ##### LineItemTransaction Validations - `claim_line_item_id` must be a valid and **active** line item for the claim. It is recommended to search for it using `.active()` and by `proc_code`, e.g. `claim.line_items.active().filter(proc_code="99215").first()` - There can be many LineItemTransactions for the same `claim_line_item_id`, but the first LineItemTransaction for a claim line item must specify either a payment or an adjustment (or allowed amount); subsequent transactions require an adjustment. - If an `adjustment` is specified, an `adjustment_code` must also be provided. - If the adjustment code is for a transfer (code starts with "Transfer"), a valid `transfer_remaining_balance_to` must be provided, and it cannot be the same payer as the `claim_coverage_id` payer. - `transfer_remaining_balance_to` can only be made to the patient (using the string `"patient"`) or to an **active** `claim_coverage_id` for the claim. - Adjustments cannot simultaneously write off and transfer the same amount; only one of `write_off` or `transfer_remaining_balance_to` should be set on LineItemTransactions where `adjustment` is present. - Adjustments and transfers are not allowed for COPAY charges, i.e. claim line items where the proc_code = `COPAY`. Only payments are allowed for those line items. - `payment` on COPAY line items must have a `claim_coverage_id` equal to `"patient"`. - `allowed` should be empty or $0 if `claim_coverage_id` is equal to `"patient"`. #### PaymentMethod Enumeration Type Enum| Value ---|--- `CASH`| cash `CHECK`| check `CARD`| card `OTHER`| other #### Example Usage The most common use case for this method will be with the [SimpleAPI](/sdk/handlers-simple-api-http/) handler. ```python from canvas_sdk.effects import Effect from canvas_sdk.v1.data import ClaimLineItem, Claim from decimal import Decimal from canvas_sdk.effects.claim import ( ClaimEffect, PaymentMethod, LineItemTransaction, ) from datetime import date from canvas_sdk.effects.simple_api import JSONResponse, Response from canvas_sdk.handlers.simple_api import APIKeyCredentials, SimpleAPIRoute class MyAPI(SimpleAPIRoute): PATH = "/routes/post-claim-payment" def authenticate(self, credentials: APIKeyCredentials) -> bool: # replace with desired authentication logic return True def get_claim_line_item(self, claim: Claim, proc_code: str) -> ClaimLineItem | None: return claim.line_items.active().filter(proc_code=proc_code).first() def create_line_item_transactions( self, charge: dict, claim: Claim, next_coverage_id: str ) -> list[LineItemTransaction]: transactions = [] if not (line_item := self.get_claim_line_item(claim, charge.get("proc_code"))): return transactions charged = Decimal(charge["charge"]) payment = Decimal(charge["paid"]) allowed = Decimal(charge["allowed"]) adjustments = charge.get("adjustment", []) first_adjustment = adjustments[0] payment = LineItemTransaction( claim_line_item_id=line_item.id, charged=charged, payment=payment, allowed=allowed, adjustment=Decimal(first_adjustment["amount"]), adjustment_code=f"{first_adjustment['group']}-{first_adjustment['code']}", # replace with whatever logic needed for resolving remaining balance transfer_remaining_balance_to="patient" if first_adjustment["group"] == "PR" else next_coverage_id, ) transactions.append(payment) additional_adjustments = adjustments[1:] for adj in additional_adjustments: transaction = LineItemTransaction( claim_line_item_id=line_item.id, adjustment=Decimal(adj["amount"]), adjustment_code=f"{adj['group']}-{adj['code']}", # replace with whatever logic needed for resolving remaining balance transfer_remaining_balance_to="patient" if adj["group"] == "PR" else next_coverage_id, ) transactions.append(transaction) return transactions def get_claim( self, account_number: str, clearinghouse_claim_id: str ) -> Claim | None: return ( Claim.objects.filter(account_number=account_number).first() or Claim.objects.filter( submissions__clearinghouse_claim_id=clearinghouse_claim_id, ).first() ) def post_payment( self, claim_payment_info: dict, check_number: str, check_date: str, payer_id: str, ) -> Effect | None: account_number = claim_payment_info.get("pcn") clearinghouse_claim_id = claim_payment_info.get("payer_icn") if not (claim := self.get_claim(account_number, clearinghouse_claim_id)): return None insurance_number = claim_payment_info.get("ins_number") if not (coverage := claim.get_coverage_by_payer_id(payer_id, insurance_number)): return None next_coverage_id = ( claim.coverages.active().exclude(payer_id=payer_id).first().id ) line_item_transactions = [] for c in claim_payment_info.get("charge", []): line_item_transactions.extend( self.create_line_item_transactions(c, claim, next_coverage_id) ) claim_effect = ClaimEffect(claim_id=claim.id) return claim_effect.post_payment( claim_coverage_id=coverage.id, line_item_transactions=line_item_transactions, method=PaymentMethod.CHECK, check_date=date.fromisoformat(check_date), check_number=check_number, deposit_date=date.fromisoformat(check_date), payment_description="Aetna 835 payment", claim_description="Payment applied via 835", ) def post(self) -> list[Response | Effect]: payment_info = self.request.json() check_number = payment_info.get("check_number") check_date = payment_info.get("paid_date") payer_id = payment_info.get("payerid") payments = [ p for claim in payment_info.get("claim", []) if (p := self.post_payment(claim, check_number, check_date, payer_id)) ] return payments + [JSONResponse({"message": "ok"})] ``` With the above plugin installed, an example call to the endpoint would look like this: ```bash curl -X POST "http://localhost:8000/plugin-io/api/pmt/routes/post-claim-payment" \ -H "Content-Type: application/json" \ -H "Authorization: " \ -d '{ "paid_date": "2025-11-06", "eraid": "23853671", "check_number": "397547083-1662491258", "paid_amount": "346.00", "payerid": "60054", "claim": [ { "pcn": "124974-1", "payer_icn": "TST397547083", "total_charge": "48", "from_dos": "20250827", "pat_name_f": "ETHYL", "ins_name_l": "BATES", "total_paid": "0", "thru_dos": null, "pat_name_l": "BATES", "ins_number": "412098745", "ins_name_f": "NORMAN", "charge": [ { "chgid": "221043771", "from_dos": "20220827", "adjustment": [{"amount": "48", "group": "OA", "code": "109"}], "paid": "0", "allowed": "0", "proc_code": "99212", "charge": "48", "thru_dos": null, "units": "1" } ] }, { "pcn": "21830-1", "payer_icn": "TST397547094", "total_charge": "75", "from_dos": "20220827", "pat_name_f": "MARYLOU", "ins_name_l": "DENNIS", "total_paid": "45", "thru_dos": null, "pat_name_l": "DENNIS", "ins_number": "223444467", "ins_name_f": "ROBERT", "charge": [ { "chgid": "221043716", "from_dos": "20220827", "adjustment": [ {"amount": "15", "group": "CO", "code": "45"}, {"amount": "10", "group": "PR", "code": "2"}, {"amount": "5", "group": "PR", "code": "3"} ], "paid": "45", "allowed": "60", "proc_code": "99213", "charge": "75", "thru_dos": null, "units": "1" } ] } ] }' ``` ### Upsert Metadata `upsert_metadata(key: str, value: str) → Effect` Upserts a key-value metadata record on a claim. If a metadata record with the given key already exists for the claim, its value will be updated. Otherwise, a new metadata record will be created. #### Parameters Parameter| Type| Description| Required ---|---|---|--- `key`| `str`| The key of the metadata| Yes `value`| `str`| The value of the metadata| Yes #### Implementation Details - Validates `claim_id` is provided and that the associated claim exists - The claim-key pair is unique; upserting with an existing key will update the value rather than creating a duplicate - If a metadata record already exists with the same claim, key, and value, no update is performed #### Example Usage ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.effects.claim import ClaimEffect from canvas_sdk.v1.data import Note class MyHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED) def compute(self) -> list[Effect]: """When a note is locked, store the lock timestamp as metadata on the claim.""" note = Note.objects.get(id=self.event.context["note_id"]) claim = note.get_claim() state = self.event.context["state"] if state == "LKD": claim_effect = ClaimEffect(claim_id=claim.id) return [claim_effect.upsert_metadata(key="locked_at", value=str(note.modified))] return [] ``` ### Add Banner `add_banner(key: str, narrative: str, intent: BannerAlertIntent, href: str | None = None) → Effect` Adds a banner alert to a claim. Banner alerts are displayed in the UI to surface important information about a claim. > **Warning:** A claim banner alert belongs to the plugin that added it. Disabling the plugin hides its banners, and [uninstalling](/sdk/canvas_cli/#canvas-uninstall) it deletes them. #### Parameters Parameter| Type| Description| Required ---|---|---|--- `key`| `str`| A unique key identifying the banner alert| Yes `narrative`| `str`| The banner text to display (max 90 characters)| Yes `intent`| BannerAlertIntent| The visual intent/severity of the banner| Yes `href`| `str`| An optional link URL for the banner| No #### BannerAlertIntent Enumeration Type Enum| Value ---|--- `INFO`| info `WARNING`| warning `ALERT`| alert #### Implementation Details - Validates `claim_id` is provided and that the associated claim exists - The `narrative` field has a maximum length of 90 characters #### Example Usage ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.effects.claim import ClaimEffect, BannerAlertIntent from canvas_sdk.v1.data import Note class MyHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED) def compute(self) -> list[Effect]: """When a note is unlocked, add a warning banner to the claim.""" note = Note.objects.get(id=self.event.context["note_id"]) claim = note.get_claim() state = self.event.context["state"] if state == "ULK": claim_effect = ClaimEffect(claim_id=claim.id) return [ claim_effect.add_banner( key="review-needed", narrative="This claim needs review before resubmission.", intent=BannerAlertIntent.WARNING, ) ] return [] ``` ### Remove Banner `remove_banner(key: str) → Effect` Removes a banner alert from a claim by its key. #### Parameters Parameter| Type| Description| Required ---|---|---|--- `key`| `str`| The unique key of the banner alert to remove| Yes #### Implementation Details - Validates `claim_id` is provided and that the associated claim exists #### Example Usage ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.effects.claim import ClaimEffect from canvas_sdk.v1.data import Note class MyHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED) def compute(self) -> list[Effect]: """When a note is locked, remove the review-needed banner from the claim.""" note = Note.objects.get(id=self.event.context["note_id"]) claim = note.get_claim() state = self.event.context["state"] if state == "LKD": claim_effect = ClaimEffect(claim_id=claim.id) return [claim_effect.remove_banner(key="review-needed")] return [] ``` ### Update Provider `update_provider(billing_provider=None, provider=None, referring_provider=None, ordering_provider=None, facility=None) → Effect` Updates provider information on a claim, including billing provider, rendering/attending provider, referring provider, ordering provider, and facility details. All parameters are optional — only the fields you provide will be updated. #### Parameters Parameter| Type| Description| Required ---|---|---|--- `billing_provider`| ClaimBillingProvider or `None`| Billing provider information| No `provider`| ClaimProvider or `None`| Rendering or attending provider information| No `referring_provider`| ClaimReferringProvider or `None`| Referring provider information| No `ordering_provider`| ClaimOrderingProvider or `None`| Ordering provider information| No `facility`| ClaimFacility or `None`| Facility information| No #### ClaimBillingProvider Attribute| Type| Description ---|---|--- `name`| `str` or `None`| Provider name (max 255 chars) `phone`| `str` or `None`| Phone number (max 15 chars) `addr1`| `str` or `None`| Address line 1 (max 255 chars) `addr2`| `str` or `None`| Address line 2 (max 255 chars) `city`| `str` or `None`| City (max 255 chars) `state`| `str` or `None`| State code (max 2 chars) `zip`| `str` or `None`| ZIP code (max 255 chars) `npi`| `str` or `None`| NPI number (max 10 chars) `tax_id`| `str` or `None`| Tax ID (max 100 chars) `tax_id_type`| `str` or `None`| Tax ID type (max 1 char) `taxonomy`| `str` or `None`| Taxonomy code (max 100 chars) `clia_number`| `str` or `None`| CLIA number (max 100 chars) #### ClaimProvider Represents the rendering or attending provider. Attribute| Type| Description ---|---|--- `first_name`| `str` or `None`| First name (max 255 chars) `last_name`| `str` or `None`| Last name (max 255 chars) `middle_name`| `str` or `None`| Middle name (max 255 chars) `npi`| `str` or `None`| NPI number (max 10 chars) `tax_id`| `str` or `None`| Tax ID (max 100 chars) `tax_id_type`| `str` or `None`| Tax ID type (max 1 char) `taxonomy`| `str` or `None`| Taxonomy code (max 100 chars) `ptan_identifier`| `str` or `None`| PTAN identifier (max 50 chars) `addr1`| `str` or `None`| Address line 1 (max 255 chars) `addr2`| `str` or `None`| Address line 2 (max 255 chars) `city`| `str` or `None`| City (max 255 chars) `state`| `str` or `None`| State code (max 2 chars) `zip`| `str` or `None`| ZIP code (max 255 chars) #### ClaimReferringProvider Attribute| Type| Description ---|---|--- `first_name`| `str` or `None`| First name (max 255 chars) `last_name`| `str` or `None`| Last name (max 255 chars) `middle_name`| `str` or `None`| Middle name (max 255 chars) `npi`| `str` or `None`| NPI number (max 10 chars) `ptan_identifier`| `str` or `None`| PTAN identifier (max 50 chars) #### ClaimOrderingProvider Attribute| Type| Description ---|---|--- `first_name`| `str` or `None`| First name (max 255 chars) `last_name`| `str` or `None`| Last name (max 255 chars) `middle_name`| `str` or `None`| Middle name (max 255 chars) `npi`| `str` or `None`| NPI number (max 10 chars) #### ClaimFacility Attribute| Type| Description ---|---|--- `name`| `str` or `None`| Facility name (max 255 chars) `npi`| `str` or `None`| NPI number (max 10 chars) `addr1`| `str` or `None`| Address line 1 (max 255 chars) `addr2`| `str` or `None`| Address line 2 (max 255 chars) `city`| `str` or `None`| City (max 255 chars) `state`| `str` or `None`| State code (max 2 chars) `zip`| `str` or `None`| ZIP code (max 255 chars) `hosp_from_date`| `date` or `None`| Hospitalization start date `hosp_to_date`| `date` or `None`| Hospitalization end date #### Implementation Details - Validates `claim_id` is provided and that the associated claim exists - Validates that the claim has existing provider information (i.e., the claim's provider record is populated) - Only fields with non-`None` values are included in the update — any fields left as `None` are excluded - Date fields (`hosp_from_date`, `hosp_to_date`) are serialized to ISO format strings #### Example Usage ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data import Claim, Note, PatientFacilityAddress from canvas_sdk.v1.data.common import AddressState from canvas_sdk.effects.claim.claim import ClaimEffect, ClaimBillingProvider, ClaimFacility class ClaimProviderHandler(BaseHandler): RESPONDS_TO = [ EventType.Name(EventType.CLAIM_CREATED), EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED), ] def get_claim(self) -> Claim | None: if self.event.type == EventType.CLAIM_CREATED: return Claim.objects.get(id=self.event.target.id) if self.event.context["state"] not in ["LKD", "PSH", "DSC"]: # claim provider details can change when notes are locked, pushed, or discharged return None return Note.objects.get(self.event.target.id).get_claim() def get_patient_facility(self, claim) -> PatientFacilityAddress | None: return PatientFacilityAddress.objects.filter( patient=claim.note.patient, state=AddressState.ACTIVE ).first() def compute(self) -> list[Effect]: """When a claim is created, or note is locked/pushed/charged, update the claim's provider information.""" if not (claim := self.get_claim()): return [] if not (facility := self.get_patient_facility(claim)): return [] billing = ClaimBillingProvider( name=facility.facility.name, phone=facility.facility.phone_number, addr1=facility.line1, addr2=facility.line2, city=facility.city, state=facility.state_code, zip=facility.postal_code, npi=facility.facility.npi_number, ) facility = ClaimFacility( name=facility.facility.name, npi=facility.facility.npi_number, addr1=facility.line1, addr2=facility.line2, city=facility.city, state=facility.state_code, zip=facility.postal_code, ) return [ ClaimEffect(claim_id=claim.id).update_provider( billing_provider=billing, facility=facility ) ] ``` ### Update Supervising Provider `update_supervising_provider(supervising_provider=None, *, staff_id=None) → Effect` Sets the supervising provider snapshot on a claim. This snapshot is captured for billing purposes (837P loop 2310D and the printed CMS-1500 form) and remains frozen after submission. Provide exactly one of: - A `staff_id` to populate the snapshot from an existing Staff record (name, NPI, taxonomy, tax ID). The snapshot remains linked to the Staff record. - A `ClaimSupervisingProvider` dataclass to specify the snapshot fields directly. This clears any Staff association. #### Parameters Parameter| Type| Description| Required ---|---|---|--- `supervising_provider`| ClaimSupervisingProvider or `None`| Free-text provider snapshot| Yes (if `staff_id` not given) `staff_id`| `str` or `None`| Staff identifier to populate from| Yes (if `supervising_provider` not given) #### ClaimSupervisingProvider The `ClaimSupervisingProvider` dataclass represents a supervising provider's identifying information for billing purposes. Attribute| Type| Description ---|---|--- `first_name`| `str` or `None`| First name (max 255 chars) `last_name`| `str` or `None`| Last name (max 255 chars) `middle_name`| `str` or `None`| Middle name (max 255 chars) `npi`| `str` or `None`| NPI number (max 10 chars) `taxonomy`| `str` or `None`| Taxonomy code (max 100 chars) `tax_id`| `str` or `None`| Tax ID (max 100 chars) `tax_id_type`| `str` or `None`| Tax ID type (max 1 char) #### Implementation Details - Validates `claim_id` is provided and that the associated claim exists - Validates that exactly one of `staff_id` or `supervising_provider` is provided - If `staff_id` is provided, validates that the Staff record exists #### Example Usage Using a Staff record: ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.effects.claim import ClaimEffect from canvas_sdk.v1.data import Note class SupervisingProviderHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED) def compute(self) -> list[Effect]: """When a note is locked, set the supervising provider from the note's supervising provider.""" note = Note.objects.get(id=self.event.context["note_id"]) claim = note.get_claim() state = self.event.context["state"] if state == "LKD" and note.supervising_provider: claim_effect = ClaimEffect(claim_id=claim.id) return [claim_effect.update_supervising_provider(staff_id=str(note.supervising_provider.id))] return [] ``` Using free-text provider information: ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.effects.claim import ClaimEffect, ClaimSupervisingProvider from canvas_sdk.v1.data import Note class SupervisingProviderHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED) def compute(self) -> list[Effect]: """When a note is locked, set a custom supervising provider on the claim.""" note = Note.objects.get(id=self.event.context["note_id"]) claim = note.get_claim() state = self.event.context["state"] if state == "LKD": claim_effect = ClaimEffect(claim_id=claim.id) return [ claim_effect.update_supervising_provider( ClaimSupervisingProvider( first_name="Jane", last_name="Doe", npi="1234567890", taxonomy="207Q00000X", ) ) ] return [] ``` ### Set Incident To `set_incident_to(value: bool) → Effect` Sets the `incident_to` billing flag for Medicare incident-to billing. When set to `True`, the claim's rendering provider fields (name, NPI, taxonomy) are automatically replaced with the supervising provider's details. #### Parameters Parameter| Type| Description| Required ---|---|---|--- `value`| `bool`| Whether the claim is billed incident-to the supervising physician| Yes #### How Incident-To Billing Works When a claim is marked as incident-to: 1. **Automatic rendering provider swap** : The rendering provider fields (first name, last name, middle name, NPI, and taxonomy) are replaced with the supervising provider's values. This swap happens immediately when `incident_to` is set to `True`, not at claim submission. 2. **Billing provider unchanged** : The billing provider information (NM1 _85 on the 837P) remains unchanged. Only the rendering provider (NM1_ 82 / Box 24J) is affected. 3. **Frozen after submission** : Once a claim has been submitted to the clearinghouse, the rendering provider swap no longer occurs even if incident-to settings change. 4. **Re-sync on supervising provider change** : If the supervising provider is updated on an incident-to claim, the rendering provider fields are automatically re-synced to match the new supervising provider. #### Printed CMS-1500 Form The supervising provider appears on the printed CMS-1500 (HCFA) form differently depending on whether the claim is marked incident-to: Scenario| Box 24J (Rendering NPI)| Box 17 (Referring/Supervising Provider) ---|---|--- **Incident-to claim**| Supervising provider's NPI (via the automatic rendering swap)| Not populated for supervising—the provider already appears in Box 24J **Non-incident-to claim with supervising provider**| Original rendering provider's NPI| Supervising provider with **DQ** qualifier (if no referring or ordering provider exists) For non-incident-to claims with a supervising provider who has a valid NPI and no referring or ordering provider, the printed form populates Box 17 with the supervising provider's name, Box 17a with the "DQ" (supervising physician) qualifier, and Box 17b with the supervising provider's NPI. Box 17 follows a priority order: referring provider (DN) takes precedence over ordering provider (DK), which takes precedence over supervising provider (DQ). The supervising provider only appears in Box 17 when no referring or ordering provider is present. #### Claim Errors The following errors prevent claim submission when incident-to is enabled: Error| Description| Solution ---|---|--- Missing supervising provider| The claim is marked incident-to but has no supervising provider with an NPI.| Add a supervising provider with a valid NPI, or disable incident-to. Supervising provider same as rendering| The supervising provider is the same as the note's original rendering provider.| Set the supervising provider to a different physician, or disable incident-to. #### Claim Warnings The following warnings are displayed for incident-to claims but do not prevent submission: Warning| Description| Guidance ---|---|--- Non-office place of service| The place of service is not office (11). Incident-to billing is generally not valid in facility settings per 42 CFR 410.26.| Confirm the place of service is correct, or disable incident-to if it does not apply. Non-Medicare payer| The payer is not Medicare. Incident-to rules are a Medicare policy; coverage varies by commercial payer.| Confirm the payer accepts incident-to billing, or disable incident-to if it does not apply. #### Implementation Details - Validates `claim_id` is provided and that the associated claim exists - The rendering provider swap requires a valid supervising provider with an NPI - The swap is skipped if the supervising provider's NPI is missing or invalid #### Example Usage ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.effects.claim import ClaimEffect from canvas_sdk.v1.data import Note class IncidentToHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.CLAIM_SUPERVISING_PROVIDER_CHANGED) def compute(self) -> list[Effect]: """When a supervising provider is set on a claim, enable incident-to billing.""" claim_id = self.event.target.id claim_effect = ClaimEffect(claim_id=claim_id) return [claim_effect.set_incident_to(True)] ``` * * * ## UpdateClaimLineItem The `UpdateClaimLineItem` effect allows you to update the `charge` field and `linked_diagnosis_codes` on a specified claim line item. Build an `UpdateClaimLineItem` with the attributes below, then return its `apply()` from your handler. ### Methods #### apply() → Effect Updates the claim line item. - Validates `claim_line_item_id` is provided and that the associated claim line item exists - If `linked_diagnosis_codes` is provided, validates that all [ClaimLineItemDiagnosisCode](/sdk/data-claim/#claimlineitemdiagnosiscode) IDs correspond to existing diagnosis codes on the claim line item - The `linked_diagnosis_codes` list represents the complete set of diagnosis codes that will be linked to the claim line item when the effect is applied. Any diagnosis codes not included in this list will be unlinked. If you wish to add a new code to the existing linked codes, you must first retrieve the current list and include all codes you want to remain linked: `list(claim_line_item.diagnosis_codes.filter(linked=True).values_list("id", flat=True)) + [new_code_id]` ### Attributes Attribute| Type| Description| Required ---|---|---|--- `claim_line_item_id`| `UUID` or `str`| Identifier for the claim line item| Yes `charge`| `float`| The charge amount to update on the claim line item| No `linked_diagnosis_codes`| `list[UUID or str]`| List of [ClaimLineItemDiagnosisCode](/sdk/data-claim/#claimlineitemdiagnosiscode) IDs to link to the claim line item| No ### Example Usage Updating charge amount. ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data import Note, ClaimLineItem from canvas_sdk.effects.claim_line_item import UpdateClaimLineItem class MyHandler(BaseHandler): """When a note is unlocked, update the associated claim's line items to have a charge of $0.00. When a note is locked, update the associated claim's line items to have a charge of $500.00.""" RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED) def get_line_items(self) -> ClaimLineItem: note = Note.objects.get(id=self.event.context["note_id"]) claim = note.get_claim() return claim.get_active_claim_line_items() def update_charge(self, id: str, charge: float) -> Effect: return UpdateClaimLineItem(claim_line_item_id=id, charge=charge).apply() def update_all_items(self, charge: float) -> list[Effect]: return [self.update_charge(line_item.id, charge) for line_item in self.get_line_items()] def compute(self) -> list[Effect]: if self.event.context["state"] == "ULK": return self.update_all_items(0.00) if self.event.context["state"] == "LKD": return self.update_all_items(500.00) return [] ``` Linking and un-linking diagnosis codes. ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data import Note, ClaimLineItem from canvas_sdk.effects.claim_line_item import UpdateClaimLineItem class MyHandler(BaseHandler): RESPONDS_TO = [ EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED), ] def compute(self) -> list[Effect]: effects = [] note = Note.objects.get(id=self.event.context["note_id"]) if not (claim := note.get_claim()): return effects state = self.event.context["state"] if state == "PSH": # only link proc codes starting with "99" to diag codes starting with "I" items = claim.line_items.filter(proc_code__startswith="99") return self.generate_effects(items, self.get_diags_that_start_with_I) if state == "LKD": # link all proc codes to all diag codes items = claim.line_items.all() return self.generate_effects(items, self.get_all_diags) if state == "ULK": # unlink proc codes starting with "99" from diag codes starting with "I" items = claim.line_items.filter(proc_code__startswith="99") return self.generate_effects(items, self.get_diags_that_dont_start_with_I) if state == "DLT": # unlink all proc codes from all diag codes items = claim.line_items.all() return self.generate_effects(items, self.get_no_diags) return effects def get_diags_that_start_with_I(self, item: ClaimLineItem) -> list[str]: return list( item.diagnosis_codes.filter(code__startswith="I").values_list( "id", flat=True ) ) def get_diags_that_dont_start_with_I(self, item: ClaimLineItem) -> list[str]: return list( item.diagnosis_codes.exclude(code__startswith="I").values_list( "id", flat=True ) ) def get_all_diags(self, item: ClaimLineItem) -> list[str]: return list(item.diagnosis_codes.values_list("id", flat=True)) def get_no_diags(self, item: ClaimLineItem) -> list[str]: return [] def generate_effects(self, items, get_diag_ids) -> list[Effect]: return [ UpdateClaimLineItem( claim_line_item_id=item.id, linked_diagnosis_codes=get_diag_ids(item) ).apply() for item in items ] ``` --- # CommandMetadata Effect Source: https://docs.canvasmedical.com/sdk/effect-command-metadata/ The `upsert_metadata` method on any command class provides a flexible key-value storage system for command-specific data within the Canvas system. This method enables the creation and updating of custom metadata entries associated with command records, allowing for extensible command information storage beyond standard command fields. ## Overview Command metadata serves as a powerful extension mechanism for storing custom command-related information that doesn't fit within the standard command data model. Metadata is managed through the `upsert_metadata` method available on all command effect classes. Metadata can be written two ways, and they store to the same place: - **From your plugin** , with the `upsert_metadata` method documented on this page. - **From the note** , with the [Command Metadata Create form effect](/sdk/command-metadata-create-form-effect/), which displays additional fields alongside a command in the chart and stores whatever a user enters as metadata against that command. Either way, the entries are readable as [CommandMetadata](/sdk/data-command/#commandmetadata) in the data module. ## Methods ### upsert_metadata(key: str, value: str) → Effect Creates or updates a metadata entry for the specified command. #### Parameters Parameter| Type| Description| Required ---|---|---|--- `key`| `str`| Unique identifier for the metadata entry within the command context| Yes `value`| `str`| The metadata value to store| Yes #### Prerequisites The command effect must be initialized with a `command_uuid`. This can be either the UUID of an existing command or the UUID of a command being originated in the same effect list. Attribute| Type| Description| Required ---|---|---|--- `command_uuid`| `str`| Id of the command record to associate metadata with| Yes #### Returns An `Effect` object configured for upserting command metadata. #### Behavior - If a metadata entry with the specified key already exists for the command, it will be updated with the new value - If no entry exists, a new metadata entry will be created - The operation is idempotent - repeated calls with the same key and value will not create duplicate entries - Raises `ValueError` if `command_uuid` is not set on the command effect ## Implementation Details ### Validation The effect performs validation at two stages: 1. **SDK Validation** : Ensures all required fields are provided before the effect is created - `command_uuid` must be set on the command effect - Both `key` and `value` must be provided 2. **Server-Side Validation** : When the effect is processed, the server verifies that the referenced command exists - Returns a descriptive error if the command is not found after all effects in the list have been processed ## Example Usage ### Basic Usage ```python from canvas_sdk.commands import PlanCommand plan = PlanCommand(command_uuid="63hdik") effect = plan.upsert_metadata(key="my_plugin:priority", value="high") ``` ### Example: Chaining with originate() You can attach metadata to a command at the same time you originate it by returning both effects in the same list: ```python import uuid from canvas_sdk.commands import PlanCommand from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class OriginateWithMetadata(BaseHandler): """Originates a plan command with metadata attached in a single operation.""" RESPONDS_TO = EventType.Name(EventType.PATIENT_CHART__SECTION__LOADED) def compute(self) -> list[Effect]: command_uuid = str(uuid.uuid4()) plan = PlanCommand( note_uuid=self.context["note_id"], command_uuid=command_uuid, narrative="Follow up in 2 weeks", ) return [ plan.originate(), plan.upsert_metadata(key="my_plugin:source", value="auto_generated"), ] ``` ### Example: Tagging a command on commit ```python from canvas_sdk.commands import PlanCommand from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class TagPlanOnCommit(BaseHandler): """Tags a plan command with a workflow stage when it is committed.""" RESPONDS_TO = EventType.Name(EventType.PLAN_COMMAND__POST_COMMIT) def compute(self) -> list[Effect]: plan = PlanCommand(command_uuid=self.event.target.id) return [plan.upsert_metadata(key="my_plugin:workflow_stage", value="committed")] ``` ### Responding to metadata events Once metadata is upserted, `COMMAND_METADATA_CREATED` and `COMMAND_METADATA_UPDATED` events are emitted and can be handled by plugins: ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data.command import CommandMetadata from logger import log class CommandMetadataListener(BaseHandler): """Reacts to command metadata changes.""" RESPONDS_TO = [ EventType.Name(EventType.COMMAND_METADATA_CREATED), EventType.Name(EventType.COMMAND_METADATA_UPDATED), ] def compute(self) -> list[Effect]: metadata = CommandMetadata.objects.get(id=self.event.target.id) log.info(f"Command {metadata.command.id}: {metadata.key}={metadata.value}") return [] ``` ## Best Practices ### Key Naming Conventions 1. **Use Descriptive Names** : Choose keys that clearly indicate the purpose of the metadata - Good: `workflow_stage`, `external_id`, `review_status` - Avoid: `data1`, `temp`, `misc` 2. **Namespace Your Keys** : Prefix keys with your plugin name to avoid collisions with other plugins - Example: `my_plugin:workflow_stage`, `my_plugin:external_id` ### Value Storage **String Serialization** : All values are stored as strings. For complex data types, serialize to JSON: ```python import json from canvas_sdk.commands import AssessCommand cmd = AssessCommand(command_uuid="abc123") data = {"reviewer": "user-id", "approved_at": "2025-01-15T10:30:00Z"} cmd.upsert_metadata(key="my_plugin:review", value=json.dumps(data)) ``` ## Notes - Metadata entries are command-specific — the same key can have different values for different commands - There is no built-in versioning; updating a key overwrites the previous value - The system does not enforce any schema on metadata values — validation is the responsibility of the implementing code - The `key` field supports up to 256 characters - To collect metadata from a user in the chart rather than writing it from a plugin, see the [Command Metadata Create form effect](/sdk/command-metadata-create-form-effect/) --- # Command Validation Source: https://docs.canvasmedical.com/sdk/effect-command-validation/ The `CommandValidationErrorEffect` returns structured error messages that are displayed to users in the Canvas UI. It serves two purposes: - **Validate a command** as it is entered in the Canvas UI, surfacing problems before it can be committed (`__POST_VALIDATION` events). - **Block a deletion** by returning the effect from a command's `__PRE_DELETE` handler. In both cases you build a `CommandValidationErrorEffect`, attach one or more error messages, and return it from your handler. Validating a command is the supported way to require fields, or to run any custom validation such as checking a free-text date entered in a questionnaire, **before a command is committed**. It is different from validating a note before it is locked: pre-lock validation only stops the note from being locked, and the command underneath it, such as a questionnaire, is still committed. Command validation blocks the commit itself and shows the error to the user. The [`command-validation` example plugin](https://github.com/Medical-Software-Foundation/canvas/tree/main/extensions/command-validation/command_validation) can be tailored to require every question on a questionnaire to be answered, or to check only specific questions or questionnaires. Where these errors are enforced differs by event: `__POST_VALIDATION` errors block a commit **only in the Canvas UI** , while `__PRE_DELETE` errors block a deletion through **both** the Canvas UI and the SDK [commands module](/sdk/commands/). Each section below covers the specifics. ## The effect ### CommandValidationErrorEffect Build a `CommandValidationErrorEffect`, add one or more errors, then return its `apply()` from your handler. #### Methods ##### add_error(message: str | ValidationError) → CommandValidationErrorEffect Appends an error, given as a message or a `ValidationError`, and returns the effect, so calls can be chained. ##### apply() → Effect Shows the errors to the user. See Validate a command and Block a deletion for what the errors block in each case. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `errors`| `list[``ValidationError``]`| List of validation errors to be displayed to the user.| No ### ValidationError Each `ValidationError` object represents a single validation error message: Attribute| Type| Description| Required ---|---|---|--- `message`| `str`| The validation error message to display. Must not be empty.| Yes ### Building the errors Add errors incrementally with `add_error()`, which returns `self` so calls can be chained: ```python effect = CommandValidationErrorEffect() effect.add_error("Narrative is required").add_error("Please provide details about the plan") return [effect.apply()] ``` Or pass a list of `ValidationError` objects to the constructor: ```python from canvas_sdk.commands.validation import CommandValidationErrorEffect, ValidationError errors = [ ValidationError("Narrative is required"), ValidationError("Narrative must be at least 10 characters long"), ] effect = CommandValidationErrorEffect(errors=errors) return [effect.apply()] ``` ## Validate a command Use `CommandValidationErrorEffect` with a command's `__POST_VALIDATION` event to check the command as it is entered and surface problems before it is committed. These events follow the pattern: `{COMMAND_KEY}_COMMAND__POST_VALIDATION` The following command types fire `__POST_VALIDATION` and can be validated with this effect: - `ADD_CONDITION_COMMAND__POST_VALIDATION` - `ADJUST_PRESCRIPTION_COMMAND__POST_VALIDATION` - `ALLERGY_COMMAND__POST_VALIDATION` - `APPROVE_REFILL_COMMAND__POST_VALIDATION` - `ASSESS_CODING_GAP_COMMAND__POST_VALIDATION` - `ASSESS_COMMAND__POST_VALIDATION` - `CANCEL_PRESCRIPTION_COMMAND__POST_VALIDATION` - `CHANGE_MEDICATION_COMMAND__POST_VALIDATION` - `CHART_SECTION_REVIEW_COMMAND__POST_VALIDATION` - `CLIPBOARD_COMMAND__POST_VALIDATION` - `CLOSE_GOAL_COMMAND__POST_VALIDATION` - `CREATE_CODING_GAP_COMMAND__POST_VALIDATION` - `DEFER_CODING_GAP_COMMAND__POST_VALIDATION` - `DENY_REFILL_COMMAND__POST_VALIDATION` - `DIAGNOSE_COMMAND__POST_VALIDATION` - `EDUCATIONAL_MATERIAL_COMMAND__POST_VALIDATION` - `FAMILY_HISTORY_COMMAND__POST_VALIDATION` - `FOLLOW_UP_COMMAND__POST_VALIDATION` - `GOAL_COMMAND__POST_VALIDATION` - `HISTORY_OF_PRESENT_ILLNESS_COMMAND__POST_VALIDATION` - `IMAGING_ORDER_COMMAND__POST_VALIDATION` - `IMMUNIZATION_STATEMENT_COMMAND__POST_VALIDATION` - `IMMUNIZE_COMMAND__POST_VALIDATION` - `INSTRUCT_COMMAND__POST_VALIDATION` - `LAB_ORDER_COMMAND__POST_VALIDATION` - `MEDICAL_HISTORY_COMMAND__POST_VALIDATION` - `MEDICATION_STATEMENT_COMMAND__POST_VALIDATION` - `ORDER_DME_COMMAND__POST_VALIDATION` - `PERFORM_COMMAND__POST_VALIDATION` - `PHYSICAL_EXAM_COMMAND__POST_VALIDATION` - `PLAN_COMMAND__POST_VALIDATION` - `POC_LAB_TEST_COMMAND__POST_VALIDATION` - `PRESCRIBE_COMMAND__POST_VALIDATION` - `QUESTIONNAIRE_COMMAND__POST_VALIDATION` - `REASON_FOR_VISIT_COMMAND__POST_VALIDATION` - `REFERENCE_COMMAND__POST_VALIDATION` - `REFER_COMMAND__POST_VALIDATION` - `REFILL_COMMAND__POST_VALIDATION` - `REMOVE_ALLERGY_COMMAND__POST_VALIDATION` - `REMOVE_PAST_MEDICAL_HISTORY_COMMAND__POST_VALIDATION` - `RESOLVE_CONDITION_COMMAND__POST_VALIDATION` - `ROS_COMMAND__POST_VALIDATION` - `SNOOZE_PROTOCOL_COMMAND__POST_VALIDATION` - `STOP_MEDICATION_COMMAND__POST_VALIDATION` - `STRUCTURED_ASSESSMENT_COMMAND__POST_VALIDATION` - `SURGICAL_HISTORY_COMMAND__POST_VALIDATION` - `TASK_COMMAND__POST_VALIDATION` - `UPDATE_DIAGNOSIS_COMMAND__POST_VALIDATION` - `UPDATE_GOAL_COMMAND__POST_VALIDATION` - `VALIDATE_CODING_GAP_COMMAND__POST_VALIDATION` - `VISUAL_EXAM_FINDING_COMMAND__POST_VALIDATION` - `VITALS_COMMAND__POST_VALIDATION` The Custom Command, Imaging Review, Lab Review, Referral Review, Uncategorized Document Review commands do **not** fire `__POST_VALIDATION`, so they can't be validated with this effect. The following handler validates a Plan command to ensure it meets specific requirements: ```python from canvas_sdk.commands import PlanCommand from canvas_sdk.commands.validation import CommandValidationErrorEffect from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from logger import log class MyHandler(BaseHandler): """ Example protocol demonstrating command validation. This protocol validates Plan commands to ensure they meet organizational requirements before being committed. """ RESPONDS_TO = EventType.Name(EventType.PLAN_COMMAND__POST_VALIDATION) def compute(self) -> list[Effect]: log.info("Running command validation protocol.") # Extract command fields from context narrative = self.context["fields"]["narrative"] # Create the validation effect effect = CommandValidationErrorEffect() # Perform validation checks if not narrative or not narrative.strip(): effect.add_error("Narrative is required and cannot be empty") elif len(narrative.strip()) < 10: effect.add_error("Narrative must be at least 10 characters long") # Check for prohibited content prohibited_terms = ["TODO", "TBD", "FIXME"] if any(term in narrative.upper() for term in prohibited_terms): effect.add_error("Narrative cannot contain placeholder text (TODO, TBD, FIXME)") # Check for required keywords (example: follow-up plans must mention timeline) if "follow" in narrative.lower() and not any(word in narrative.lower() for word in ["week", "month", "day"]): effect.add_error("Follow-up plans must include a specific timeline") # Return the effect return [effect.apply()] ``` When validation errors are returned, the Canvas UI shows them to the user — the command's action buttons are disabled and the messages appear as a tooltip — so the command can't be committed there. Multiple errors can be returned at once, and all are displayed. On a Refill command, the error disables the command's **REVIEW** button, so the user can't reach **Sign and send** , **Sign** , or **Print**. The message appears as a tooltip on the disabled button. The [`example_commands` example plugin](https://github.com/canvas-medical/canvas-plugins/tree/main/example-plugins/example_commands) blocks every refill this way; add a condition to its handler to block refills selectively. Return the error from the `__POST_VALIDATION` event, not `__PRE_COMMIT`. On a Refill command, a validation error returned from `REFILL_COMMAND__PRE_COMMIT` is ignored, and the refill commits anyway. > **Note:** `__POST_VALIDATION` only gates committing **in the Canvas UI**. A `.commit()` made through the SDK [commands module](/sdk/commands/) is **not** blocked by these errors — the command still commits. Use it as a UI guardrail, not as an enforced rule on SDK-driven commits. (Blocking a deletion, below, _does_ work through both the UI and the SDK.) ## Block a deletion Return a `CommandValidationErrorEffect` from a command's `__PRE_DELETE` handler to block its deletion. Unlike `__POST_VALIDATION`, this works through **both** the Canvas UI and the SDK [commands module](/sdk/commands/): the deletion is aborted, the surrounding transaction is rolled back, and the error messages are returned to whatever initiated the delete — a [`delete()`](/sdk/commands/) call or a delete in the UI. For SDK-initiated deletes, the error is written to `canvas logs`. Pre-delete events follow the pattern: `{COMMAND_KEY}_COMMAND__PRE_DELETE` `__PRE_DELETE` is fired by the following command types (every command except Chart Section Review): - `ADD_CONDITION_COMMAND__PRE_DELETE` - `ADJUST_PRESCRIPTION_COMMAND__PRE_DELETE` - `ALLERGY_COMMAND__PRE_DELETE` - `APPROVE_REFILL_COMMAND__PRE_DELETE` - `ASSESS_CODING_GAP_COMMAND__PRE_DELETE` - `ASSESS_COMMAND__PRE_DELETE` - `CANCEL_PRESCRIPTION_COMMAND__PRE_DELETE` - `CHANGE_MEDICATION_COMMAND__PRE_DELETE` - `CLIPBOARD_COMMAND__PRE_DELETE` - `CLOSE_GOAL_COMMAND__PRE_DELETE` - `CREATE_CODING_GAP_COMMAND__PRE_DELETE` - `CUSTOM_COMMAND_COMMAND__PRE_DELETE` - `DEFER_CODING_GAP_COMMAND__PRE_DELETE` - `DENY_REFILL_COMMAND__PRE_DELETE` - `DIAGNOSE_COMMAND__PRE_DELETE` - `EDUCATIONAL_MATERIAL_COMMAND__PRE_DELETE` - `FAMILY_HISTORY_COMMAND__PRE_DELETE` - `FOLLOW_UP_COMMAND__PRE_DELETE` - `GOAL_COMMAND__PRE_DELETE` - `HISTORY_OF_PRESENT_ILLNESS_COMMAND__PRE_DELETE` - `IMAGING_ORDER_COMMAND__PRE_DELETE` - `IMAGING_REVIEW_COMMAND__PRE_DELETE` - `IMMUNIZATION_STATEMENT_COMMAND__PRE_DELETE` - `IMMUNIZE_COMMAND__PRE_DELETE` - `INSTRUCT_COMMAND__PRE_DELETE` - `LAB_ORDER_COMMAND__PRE_DELETE` - `LAB_REVIEW_COMMAND__PRE_DELETE` - `MEDICAL_HISTORY_COMMAND__PRE_DELETE` - `MEDICATION_STATEMENT_COMMAND__PRE_DELETE` - `ORDER_DME_COMMAND__PRE_DELETE` - `PERFORM_COMMAND__PRE_DELETE` - `PHYSICAL_EXAM_COMMAND__PRE_DELETE` - `PLAN_COMMAND__PRE_DELETE` - `POC_LAB_TEST_COMMAND__PRE_DELETE` - `PRESCRIBE_COMMAND__PRE_DELETE` - `QUESTIONNAIRE_COMMAND__PRE_DELETE` - `REASON_FOR_VISIT_COMMAND__PRE_DELETE` - `REFERENCE_COMMAND__PRE_DELETE` - `REFERRAL_REVIEW_COMMAND__PRE_DELETE` - `REFER_COMMAND__PRE_DELETE` - `REFILL_COMMAND__PRE_DELETE` - `REMOVE_ALLERGY_COMMAND__PRE_DELETE` - `REMOVE_PAST_MEDICAL_HISTORY_COMMAND__PRE_DELETE` - `RESOLVE_CONDITION_COMMAND__PRE_DELETE` - `ROS_COMMAND__PRE_DELETE` - `SNOOZE_PROTOCOL_COMMAND__PRE_DELETE` - `STOP_MEDICATION_COMMAND__PRE_DELETE` - `STRUCTURED_ASSESSMENT_COMMAND__PRE_DELETE` - `SURGICAL_HISTORY_COMMAND__PRE_DELETE` - `TASK_COMMAND__PRE_DELETE` - `UNCATEGORIZED_DOCUMENT_REVIEW_COMMAND__PRE_DELETE` - `UPDATE_DIAGNOSIS_COMMAND__PRE_DELETE` - `UPDATE_GOAL_COMMAND__PRE_DELETE` - `VALIDATE_CODING_GAP_COMMAND__PRE_DELETE` - `VISUAL_EXAM_FINDING_COMMAND__PRE_DELETE` - `VITALS_COMMAND__PRE_DELETE` The following handler prevents deletion of a Refer command once its priority has been set to `Urgent` or `STAT`, so high-priority referrals can't be removed by mistake. The command's field values are available on the event context, so no extra lookup is needed: ```python from canvas_sdk.commands import ReferCommand from canvas_sdk.commands.validation import CommandValidationErrorEffect from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class BlockUrgentReferralDeletionHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.REFER_COMMAND__PRE_DELETE) def compute(self) -> list[Effect]: priority = self.context["fields"].get("priority") protected = {ReferCommand.Priority.URGENT.value, ReferCommand.Priority.STAT.value} if priority in protected: effect = CommandValidationErrorEffect() effect.add_error( f"A {priority}-priority referral can't be deleted. " "Lower its priority first if you need to remove it." ) return [effect.apply()] return [] ``` When a delete is attempted on an `Urgent` or `STAT` referral, it is blocked and the error message is returned to whoever initiated it. For more information about command events and their context objects, see the [Events documentation](/sdk/events/). --- # Compound Medication Effects Source: https://docs.canvasmedical.com/sdk/effect-compound-medication/ The Compound Medication effects enable the creation and management of compound medication formulations within the Canvas system. These effects support the customization of medications prepared by compounding pharmacies according to prescriptions. Build a `CompoundMedication` with the attributes you want to set, then return one of its two methods from your handler: `create()` for a new compound medication, or `update()` for an existing one. ## Methods ### create() → Effect Creates a compound medication. - `formulation`, `potency_unit_code`, and `controlled_substance` are required. - `controlled_substance_ndc` is also required when `controlled_substance` is anything other than `"N"`. See Creating a compound medication for an example. ### update() → Effect Updates an existing compound medication. - `instance_id` is required, and must be the id of an existing compound medication. - Only the attributes you set on the effect are changed. Attributes you leave unset keep their current values. See Updating a compound medication for an example. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `str` or `None`| The id of the compound medication to update| For `update()` `formulation`| `str` or `None`| The compound medication formulation (max 105 characters)| For `create()` `potency_unit_code`| `str` or `None`| The unit of measurement for the medication| For `create()` `controlled_substance`| `str` or `None`| The controlled substance schedule| For `create()` `controlled_substance_ndc`| `str` or `None`| NDC code for controlled substances (dashes removed)| When `controlled_substance` is not `"N"` `active`| `bool` or `None`| Whether the compound medication is active. Defaults to `True` on creation.| No ## Example Usage ### Creating a compound medication ```python from canvas_sdk.effects.compound_medications import CompoundMedication as CompoundMedicationEffect from canvas_sdk.handlers.base import BaseHandler from canvas_sdk.events import EventType from canvas_sdk.v1.data.compound_medication import CompoundMedication as CompoundMedicationModel class CompoundMedicationCreator(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.PATIENT_CREATED)] def compute(self): # Create a non-controlled compound medication compound_med = CompoundMedicationEffect( formulation="Testosterone 200mg/mL in Grapeseed Oil", potency_unit_code=CompoundMedicationModel.PotencyUnit.Milliliter, controlled_substance=CompoundMedicationModel.ControlledSubstanceSchedule.SCHEDULE_NOT_SCHEDULED, active=True ) # Create a controlled substance compound medication controlled_compound = CompoundMedicationEffect( formulation="Hydrocodone 5mg/Acetaminophen 325mg Capsule", potency_unit_code=CompoundMedicationModel.PotencyUnit.Capsule, controlled_substance=CompoundMedicationModel.ControlledSubstanceSchedule.SCHEDULE_II, controlled_substance_ndc="12345678901", active=True ) return [compound_med.create(), controlled_compound.create()] ``` ### Updating a compound medication ```python from canvas_sdk.effects.compound_medications import CompoundMedication as CompoundMedicationEffect from canvas_sdk.handlers.base import BaseHandler from canvas_sdk.v1.data.compound_medication import CompoundMedication as CompoundMedicationModel from canvas_sdk.events import EventType class CompoundMedicationUpdater(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.PLUGIN_CREATED)] def compute(self): # Find a compound medication to update compound_med = CompoundMedicationModel.objects.filter( formulation__contains="Testosterone" ).first() if compound_med: # Update to make it a controlled substance update_effect = CompoundMedicationEffect( instance_id=str(compound_med.id), controlled_substance="III", controlled_substance_ndc="98765432101" ) return [update_effect.update()] return [] ``` ## Implementation Details - **Formulation Validation** : The formulation field is limited to 105 characters - **NDC Formatting** : Any dashes in the NDC code are automatically removed during processing - **Cross-field Validation** : When a controlled substance schedule is specified (anything other than "N"), an NDC code must be provided - **Default Values** : If not specified, `active` defaults to `True` for new compound medications - **Potency Unit Codes** : Must use valid codes as defined in the [PotencyUnit](/sdk/data-compound-medication/#potencyunit) enumeration - **Controlled Substance Schedules** : Must use valid values as defined in the [ControlledSubstanceSchedule](/sdk/data-compound-medication/#controlledsubstanceschedule) enumeration ## Validation Both methods perform validation before execution: ### Create Effect Validation: - Validates all required fields are provided - Ensures `potency_unit_code` is a valid value from the PotencyUnit enumeration - Ensures `controlled_substance` is a valid schedule from the ControlledSubstanceSchedule enumeration - Validates NDC is provided for controlled substances (when schedule is not "N") - Checks formulation length does not exceed 105 characters ### Update Effect Validation: - Verifies the compound medication exists before updating - Validates any provided fields follow the same rules as creation - Ensures NDC is provided if updating to a controlled substance - Only updates fields that are explicitly provided (partial updates supported) ## Error Handling If validation fails, a `ValidationError` is raised with detailed error messages indicating which fields failed validation and why. Error messages are aggregated to provide comprehensive feedback about all validation failures at once. --- # Configure Command Buttons Source: https://docs.canvasmedical.com/sdk/effect-configure-command-buttons/ The `ConfigureCommandButtons` effect allows plugins to hide or disable the command buttons that appear in specific areas of the patient chart — such as the conditions section, medications section, or protocol cards. Build a `ConfigureCommandButtons` with the attributes you want to set, then return its `apply()` from your handler. ## Methods ### apply() → Effect Applies the button visibility to the patient's chart. - `patient_id` is required. - Each `location` may appear only once in `locations`; a duplicate raises a `ValidationError`. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `patient_id`| `str`| The id of the [patient](/sdk/data-patient/).| Yes `locations`| `list[``LocationConfig``]`| Areas to configure. Areas not listed retain their default visible state.| No ## LocationConfig Each entry in `locations` is a `LocationConfig` with the following attributes, both required: Attribute| Type| Description ---|---|--- `location`| `Location`| The chart area to configure `visibility`| `Visibility`| The visibility state for that area ## Locations The `Location` enum defines which areas of the chart can be configured: Value| Area ---|--- `CONDITIONS`| Conditions chart summary section `MEDICATIONS`| Medications chart summary section `ALLERGIES`| Allergies chart summary section `GOALS`| Goals chart summary section `VITALS`| Vitals chart summary section `IMMUNIZATIONS`| Immunizations chart summary section `SURGICAL_HISTORY`| Surgical history chart summary section `FAMILY_HISTORY`| Family history chart summary section `SOCIAL_DETERMINANTS`| Social determinants chart summary section `CARE_TEAMS`| Care teams chart summary section `CODING_GAPS`| Coding gaps chart summary section `QUALITY_PROTOCOLS`| Quality protocol result cards `LAB_REVIEWS`| Lab report review result cards `IMAGING_REVIEWS`| Imaging report review result cards `REFERRAL_REVIEWS`| Referral report review result cards `DOCUMENT_REVIEWS`| Uncategorized document review result cards ## Visibility Each location can be configured with one of three visibility values: Value| Behavior ---|--- `VISIBLE`| Buttons are shown and interactive (default when not listed) `HIDDEN`| Buttons are not rendered `DISABLED`| Buttons are rendered but not interactive ## Example: Patient Chart Load `PATIENT_TIMELINE__GET_CONFIGURATION` fires every time a patient chart is opened, making it a convenient hook for configuring button visibility on chart load. The example below hides all command buttons across every location: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.configure_command_buttons import ConfigureCommandButtons from canvas_sdk.events import EventType from canvas_sdk.protocols import BaseProtocol Location = ConfigureCommandButtons.Location LocationConfig = ConfigureCommandButtons.LocationConfig Visibility = ConfigureCommandButtons.Visibility class HideButtonsOnChartLoad(BaseProtocol): RESPONDS_TO = EventType.Name(EventType.PATIENT_TIMELINE__GET_CONFIGURATION) def compute(self) -> list[Effect]: return [ ConfigureCommandButtons( patient_id=self.target, locations=[ LocationConfig(location=loc, visibility=Visibility.HIDDEN) for loc in Location ], ).apply() ] ``` ## Example: Note Applications One use case is toggling chart buttons alongside a `NoteApplication`. When the application tab opens, `on_open` disables chart buttons. When the provider switches back to the note body, Canvas sends a `NOTE_TAB_CHANGE` message to the iframe, which calls a `SimpleAPI` endpoint to restore them. ### Python ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.configure_command_buttons import ConfigureCommandButtons from canvas_sdk.effects.launch_modal import LaunchModalEffect from canvas_sdk.effects.simple_api import JSONResponse, Response from canvas_sdk.handlers.application import NoteApplication from canvas_sdk.handlers.simple_api import SimpleAPI, StaffSessionAuthMixin, api from canvas_sdk.templates import render_to_string Location = ConfigureCommandButtons.Location LocationConfig = ConfigureCommandButtons.LocationConfig Visibility = ConfigureCommandButtons.Visibility class MyChartingApp(NoteApplication): NAME = "My Charting App" IDENTIFIER = "my-plugin:charting-app" def on_open(self) -> list[Effect]: patient_id = self.event.context.get("patient", {}).get("id") return [ LaunchModalEffect( target=LaunchModalEffect.TargetType.NOTE, content=render_to_string( "templates/charting_app.html", context={"identifier": self.IDENTIFIER}, ), title="My Charting App", ).apply(), ConfigureCommandButtons( patient_id=patient_id, locations=[ LocationConfig(location=loc, visibility=Visibility.DISABLED) for loc in Location ], ).apply(), ] class CommandButtonsApi(StaffSessionAuthMixin, SimpleAPI): @api.post("/configure-buttons/disable") def disable(self) -> list[Response | Effect]: patient_id = self.request.json().get("patient_id") return [ JSONResponse({"ok": True}), ConfigureCommandButtons( patient_id=patient_id, locations=[ LocationConfig(location=loc, visibility=Visibility.DISABLED) for loc in Location ], ).apply(), ] @api.post("/configure-buttons/enable") def enable(self) -> list[Response | Effect]: patient_id = self.request.json().get("patient_id") return [ JSONResponse({"ok": True}), ConfigureCommandButtons( patient_id=patient_id, locations=[ LocationConfig(location=loc, visibility=Visibility.VISIBLE) for loc in Location ], ).apply(), ] ``` ### Template The iframe listens for `NOTE_TAB_CHANGE` messages from Canvas and calls the appropriate endpoint. When `tab` is `"note"` the provider has switched back to the note body; when `tab` matches the application's identifier the application tab is active. ```html ``` Both `MyChartingApp` and `CommandButtonsApi` should be registered as `handlers` in your [`CANVAS_MANIFEST.json`](/sdk/canvas_manifest/#handlers). --- # Coverage Effect Source: https://docs.canvasmedical.com/sdk/effect-coverage/ The `Coverage` effect writes a patient's insurance coverage from a plugin, including the front and back images of the insurance card. Use it to capture insurance during intake, such as from a patient portal form where the patient photographs their card. You can also use it to keep coverages in step with an external system. The `CoverageReorder` effect changes the rank and stack of several coverages at once. Canvas applies the same rules to these effects as it does to the coverage form in the patient's chart. The effects also check most of those rules in your plugin before the effect is sent. An invalid coverage raises a validation error in your handler. To read coverages, see the [Coverage data model](/sdk/data-coverage/). ## Methods Build a `Coverage` with the attributes you want to set, then return one of its methods from your handler. ### create() → Effect Creates a coverage for a patient. - `patient_id`, `issuer_id`, `coverage_rank`, `id_number`, and `patient_relationship_to_subscriber` are required. - `coverage_id` must not be set. - The patient must not already have a coverage with the same `coverage_rank` in the same stack. A coverage that ends today or earlier, in the instance's time zone, frees its rank. To replace a primary coverage, expire the old one with today's date, then create the new one the same day. - `stack` defaults to `IN_USE`, and `coverage_start_date` defaults to today in the instance's time zone. - Canvas requests an eligibility check after the coverage is saved. ### update() → Effect Changes an existing coverage. - `coverage_id` is required, and must be the id of an existing coverage. - Only the attributes you set on the effect are changed. Setting an optional attribute to `None` clears it. - The required attributes, `stack`, `subscriber_id`, and `coverage_start_date` cannot be set to `None` or an empty string. - `patient_id` cannot be changed. - If you change `issuer_id`, Canvas clears the coverage's `issuer_address_id`, `issuer_phone_id`, and `coverage_type`, unless you set new values in the same update. - If another coverage in the same stack already holds the new rank, the coverage you updated takes it. The other coverage moves down one rank. - Canvas requests a new eligibility check when the update changes `issuer_id`, `issuer_address_id`, `id_number`, `subscriber_id`, or `patient_relationship_to_subscriber`. ### expire(coverage_end_date) → Effect Ends an existing coverage on `coverage_end_date`, a `datetime.date`. `coverage_id` is required. The end date must be on or after the coverage's start date; a coverage can end on the day it starts. Canvas records an audit event, as it does when a coverage is expired from the chart. ### remove() → Effect Moves an existing coverage to the `REMOVED` stack. `coverage_id` is required. Canvas records an audit event. `create()`, `update()`, and `CoverageReorder` do not accept the `REMOVED` stack. Use `remove()` instead. ### remove_photo(side) → Effect Removes the front or back card image from an existing coverage. `coverage_id` is required. Set `side` to `PhotoSide.FRONT` or `PhotoSide.BACK`. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `coverage_id`| `UUID` or `str`| The `id` of an existing [Coverage](/sdk/data-coverage/#coverage). Must be unset when creating a coverage.| For every method except `create()` `patient_id`| `str`| The `id` of the [Patient](/sdk/data-patient/#patient) the coverage belongs to.| For `create()` `issuer_id`| `UUID` or `str`| The `id` of the payer, a [Transactor](/sdk/data-coverage/#transactor). The coverage's plan type comes from the payer.| For `create()` `issuer_address_id`| `UUID` or `str` or `None`| The `id` of a [TransactorAddress](/sdk/data-coverage/#transactoraddress) that belongs to the payer.| No `issuer_phone_id`| `UUID` or `str` or `None`| The `id` of a [TransactorPhone](/sdk/data-coverage/#transactorphone) that belongs to the payer.| No `patient_relationship_to_subscriber`| [`CoverageRelationshipCode`](/sdk/data-coverage/#coveragerelationshipcode)| The patient's relationship to the subscriber. See Subscribers.| For `create()` `subscriber_id`| `str`| The `id` of the [Patient](/sdk/data-patient/#patient) who holds the policy. See Subscribers.| When the patient is not the subscriber `subscriber_identifier`| `str` or `None`| The subscriber's identifier on the policy. Up to 100 characters.| No `coverage_rank`| [`CoverageRank`](/sdk/data-coverage/#coveragerank)| The coverage's order within its stack: `PRIMARY` (1), `SECONDARY` (2), `TERTIARY` (3), `QUATERNARY` (4), or `QUINARY` (5).| For `create()` `stack`| [`CoverageStack`](/sdk/data-coverage/#coveragestack)| `IN_USE` or `OTHER`. Defaults to `IN_USE` on create.| No `coverage_type`| [`TransactorCoverageType`](/sdk/data-coverage/#transactorcoveragetype) or `None`| The type of coverage.| No `id_number`| `str`| The member ID on the insurance card. Up to 100 characters.| For `create()` `plan`| `str` or `None`| Plan name. Up to 255 characters.| No `sub_plan`| `str` or `None`| Sub-plan name. Up to 255 characters.| No `group`| `str` or `None`| Group number. Up to 255 characters.| No `sub_group`| `str` or `None`| Sub-group number. Up to 255 characters.| No `employer`| `str` or `None`| The subscriber's employer. Up to 255 characters.| No `coverage_start_date`| `datetime.date`| The date the coverage starts. Must be on or before `coverage_end_date`. Defaults to today on create.| No `coverage_end_date`| `datetime.date` or `None`| The date the coverage ends.| No `comments`| `str` or `None`| Free-text comments.| No `card_image_front_upload_key`| `str` or `None`| The upload key of the front image of the insurance card. See Card images.| No `card_image_back_upload_key`| `str` or `None`| The upload key of the back image of the insurance card. See Card images.| No `CoverageRank`, `CoverageRelationshipCode`, `CoverageStack`, `TransactorCoverageType`, and `Transactor` are imported from `canvas_sdk.v1.data.coverage`. `PhotoSide` is imported from `canvas_sdk.effects.coverage`. ### Subscribers When `patient_relationship_to_subscriber` is `CoverageRelationshipCode.SELF`, the patient is the subscriber. Leave `subscriber_id` unset, or set it to the patient's `id`. For any other relationship, set `subscriber_id` to the `id` of a different patient, the person who holds the policy. ## Card images Card images are PNG or JPEG files that a user uploads to your plugin through a SimpleAPI route declared with `file_uploads="stored"`. Set `card_image_front_upload_key` and `card_image_back_upload_key` to the `key` of each uploaded `StoredFilePart`. See [Storing uploaded files](/sdk/handlers-simple-api-http/#storing-uploaded-files). Each key must come from a file your plugin uploaded, and each file must be a PNG or JPEG image. When the effect is applied, Canvas copies the file to the coverage and then deletes the upload, so each key works only once. If any part of the effect fails, Canvas saves none of it, including the card images. On `update()`, setting one image key replaces that side of the card and leaves the other side unchanged. A key of `None` attaches no image, so that side also keeps its current image. To remove an image, use `remove_photo()`. For working code, see Capture an insurance card from a form, which creates a coverage from uploaded card photos, and Expire or remove a coverage, or remove a card image, which removes one side of the card. ## Reordering coverages `CoverageReorder` sets the rank and stack of several of a patient's coverages in one effect. Build it with the patient's `id` and an `ordering` list of `CoverageReorderEntry` objects, then return `apply()`. Attribute| Type| Description ---|---|--- `patient_id`| `str`| The `id` of the [Patient](/sdk/data-patient/#patient). `ordering`| `list[CoverageReorderEntry]`| The new position of each coverage. Must contain at least one entry. Each `CoverageReorderEntry` has these attributes, all required: Attribute| Type| Description ---|---|--- `coverage_id`| `UUID` or `str`| The `id` of one of the patient's [coverages](/sdk/data-coverage/#coverage). `coverage_rank`| [`CoverageRank`](/sdk/data-coverage/#coveragerank)| The coverage's new rank. `stack`| [`CoverageStack`](/sdk/data-coverage/#coveragestack)| The coverage's new stack: `IN_USE` or `OTHER`. The ordering must follow these rules: - Each coverage appears once and belongs to the patient. - Within each stack, ranks are unique and consecutive, starting at 1. - For each stack the ordering places a coverage in, the ordering lists every active coverage in that stack. For working code, see Swap primary and secondary coverages. ## Examples ### Capture an insurance card from a form This SimpleAPI route creates a primary coverage from a `multipart/form-data` request. The request carries the patient's id, the payer's id, the member ID, and photos of both sides of the card. The route uses the [API key mixin](/sdk/handlers-simple-api-http/#api-key-1), so declare the `simpleapi-api-key` secret in your manifest and set it on your instance: ```python from http import HTTPStatus from canvas_sdk.effects import Effect from canvas_sdk.effects.coverage import Coverage from canvas_sdk.effects.simple_api import JSONResponse, Response from canvas_sdk.handlers.simple_api import APIKeyAuthMixin, SimpleAPI, api from canvas_sdk.v1.data.coverage import CoverageRank, CoverageRelationshipCode class InsuranceCardAPI(APIKeyAuthMixin, SimpleAPI): @api.post("/insurance-card", file_uploads="stored") def insurance_card(self) -> list[Response | Effect]: form = self.request.form_data() front = form["card_front"] back = form["card_back"] if front.error or back.error: return [JSONResponse({"error": "upload failed"}, status_code=HTTPStatus.BAD_GATEWAY)] coverage = Coverage( patient_id=form["patient_id"].value, issuer_id=form["payer_id"].value, id_number=form["member_id"].value, coverage_rank=CoverageRank.PRIMARY, patient_relationship_to_subscriber=CoverageRelationshipCode.SELF, card_image_front_upload_key=front.key, card_image_back_upload_key=back.key, ) return [coverage.create(), JSONResponse({}, status_code=HTTPStatus.ACCEPTED)] ``` Call the route with a `multipart/form-data` POST. Each form field name matches a key the handler reads, the two card photos are sent as files, and the API key goes in the `Authorization` header: ```shell curl --request POST \ --url 'https://.canvasmedical.com/plugin-io/api//insurance-card' \ --header 'Authorization: ' \ --form 'patient_id=b80b1cdc2e6a4aca90ccebc02e683f35' \ --form 'payer_id=0f3b6c1e-9a2d-4e7b-8c5f-1d2e3a4b5c6d' \ --form 'member_id=W123456789' \ --form 'card_front=@card-front.jpg;type=image/jpeg' \ --form 'card_back=@card-back.jpg;type=image/jpeg' ``` The same request from a Python script, uploading two card photos saved on disk, using the [`requests`](https://requests.readthedocs.io/) library: ```python from pathlib import Path import requests url = "https://.canvasmedical.com/plugin-io/api//insurance-card" front_path = Path("card-front.jpg") back_path = Path("card-back.jpg") with front_path.open("rb") as front, back_path.open("rb") as back: response = requests.post( url, headers={"Authorization": ""}, data={ "patient_id": "b80b1cdc2e6a4aca90ccebc02e683f35", "payer_id": "0f3b6c1e-9a2d-4e7b-8c5f-1d2e3a4b5c6d", "member_id": "W123456789", }, files={ "card_front": (front_path.name, front, "image/jpeg"), "card_back": (back_path.name, back, "image/jpeg"), }, timeout=30, ) print(response.status_code) ``` Passing `files` makes `requests` send the request as `multipart/form-data`, with each `data` entry as a plain form field. Each file is a `(filename, file, content_type)` tuple; open it in binary mode. `curl --form` sends the request as `multipart/form-data` too. A browser form does the same with `enctype="multipart/form-data"`, or a `FormData` object passed to `fetch`. The route answers: - `202 Accepted` when both photos were stored and the coverage effect was returned. - `502 Bad Gateway` when a photo failed to store. Canvas returns this before calling the handler when every file fails; the handler returns it when only one does. - `400 Bad Request` when the request isn't `multipart/form-data`. - `401 Unauthorized` when the API key is missing or wrong. To find a payer's `id`, query the [Transactor](/sdk/data-coverage/#transactor) model: ```python from canvas_sdk.v1.data.coverage import Transactor payer = Transactor.objects.filter(name__icontains="aetna", active=True).first() ``` ### Update a coverage ```python from canvas_sdk.effects.coverage import Coverage from canvas_sdk.v1.data.coverage import Coverage as CoverageModel coverage = CoverageModel.objects.get(id="b3b5e4f6-1c2d-4e8a-9f0b-7a6c5d4e3f21") # Change the group number and clear the comments effect = Coverage(coverage_id=coverage.id, group="GRP-2027", comments=None).update() ``` ### Expire or remove a coverage, or remove a card image ```python from datetime import date from canvas_sdk.effects.coverage import Coverage, PhotoSide coverage_id = "b3b5e4f6-1c2d-4e8a-9f0b-7a6c5d4e3f21" expire = Coverage(coverage_id=coverage_id).expire(coverage_end_date=date(2026, 12, 31)) remove = Coverage(coverage_id=coverage_id).remove() remove_back = Coverage(coverage_id=coverage_id).remove_photo(side=PhotoSide.BACK) ``` ### Swap primary and secondary coverages ```python from canvas_sdk.effects.coverage import CoverageReorder, CoverageReorderEntry from canvas_sdk.v1.data.coverage import CoverageRank, CoverageStack effect = CoverageReorder( patient_id="d7af3e356368446c85b40a5d6ff7288e", ordering=[ CoverageReorderEntry( coverage_id="b3b5e4f6-1c2d-4e8a-9f0b-7a6c5d4e3f21", coverage_rank=CoverageRank.PRIMARY, stack=CoverageStack.IN_USE, ), CoverageReorderEntry( coverage_id="4a9e2c71-8d3b-4f5e-a6c0-1b2d3e4f5a6b", coverage_rank=CoverageRank.SECONDARY, stack=CoverageStack.IN_USE, ), ], ).apply() ``` --- # CreateCCDA Source: https://docs.canvasmedical.com/sdk/effect-create-ccda-export/ The `CreateCCDA` effect creates a C-CDA document for a patient with the provided XML content. This effect allows plugins to store C-CDA XML documents, which can be used for clinical document exchange, patient summaries, or referrals. This effect stores a C-CDA document, generating or otherwise sourcing that document is the responsibility of the plugin. Build a `CreateCCDA` and return its `apply()` from your handler. ## Methods ### apply() → Effect Creates the C-CDA document for the patient. - `patient_id` and `content` are required. `patient_id` must be the id of an existing patient, and `content` must be valid XML. - `document_type` defaults to `CCD`. ## Attributes Name| Type| Description| Required ---|---|---|--- `patient_id`| `str`| The id of the [patient](/sdk/data-patient/).| Yes `content`| `str`| The C-CDA XML content as a string. Must be valid XML.| Yes `document_type`| `DocumentType`| Type of C-CDA document. Defaults to `DocumentType.CCD`.| No ## DocumentType Enum Value| Description ---|--- `CCD`| Continuity of Care Document (default) `REFERRAL`| Referral document ## Validation The effect performs the following validations before execution: - **Patient Exists** : Verifies that a patient with the given `patient_id` exists in the system. - **Valid XML** : Validates that the `content` field contains well-formed XML. Malformed XML will result in a validation error. - **Required Fields** : Both `patient_id` and `content` must be non-empty strings. ## Example Usage ### Basic CCD Creation ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.ccda import CreateCCDA, DocumentType from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class MyHandler(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.PATIENT_UPDATED)] def compute(self) -> list[Effect]: # Sample C-CDA XML content ccda_xml = """ Patient Summary """ effect = CreateCCDA( patient_id="a1b2c3d4-e5f6-7890-abcd-ef1234567890", content=ccda_xml, document_type=DocumentType.CCD, ) return [effect.apply()] ``` ### Creating a Referral Document ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.ccda import CreateCCDA, DocumentType from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class MyHandler(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.PATIENT_UPDATED)] def compute(self) -> list[Effect]: referral_xml_content = "..." effect = CreateCCDA( patient_id="a1b2c3d4-e5f6-7890-abcd-ef1234567890", content=referral_xml_content, document_type=DocumentType.REFERRAL, ) return [effect.apply()] ``` ### Using Default Document Type When `document_type` is not specified, it defaults to `CCD`: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.ccda import CreateCCDA from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class MyHandler(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.PATIENT_UPDATED)] def compute(self) -> list[Effect]: ccda_xml = "..." effect = CreateCCDA( patient_id="a1b2c3d4-e5f6-7890-abcd-ef1234567890", content=ccda_xml, ) # document_type defaults to DocumentType.CCD return [effect.apply()] ``` ## Use Cases - **Clinical Document Exchange** : Generate C-CDAs for sharing patient information with external systems or providers. - **Patient Summaries** : Create Continuity of Care Documents containing a patient's clinical summary. - **Referral Documentation** : Generate referral documents when referring patients to specialists. - **Integration with External Systems** : Produce standardized C-CDA documents for healthcare interoperability. ## Notes - The C-CDA XML content is stored as a file on the patient's record. You can access the record in Settings > CCDAs to be able to view, transmit, or download the file. --- # CreatePatientExternalIdentifier Source: https://docs.canvasmedical.com/sdk/effect-create-patient-external-identifier/ Creates a new external identifier for a patient. ## Methods ### create() → Effect Creates the external identifier on the patient. - `value` and `patient_id` are required. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `patient_id`| `str`| The id of the [patient](/sdk/data-patient/).| Yes `system`| `str`| The system for the external identifier (url).| No `value`| `str`| The value of the external identifier.| Yes ## Example ```python from canvas_sdk.effects.patient import CreatePatientExternalIdentifier effect = CreatePatientExternalIdentifier( patient_id="1eed3ea2a8d546a1b681a2a45de1d790", system="https://www.va.gov/", value="VET123456" ) effect.create() ``` This effect will create a new external identifier for the specified patient. --- # CreatePatientPreferredPharmacies Source: https://docs.canvasmedical.com/sdk/effect-create-patient-preferred-pharmacies/ Creates preferred pharmacies for a patient. ## Methods ### create() → Effect Sets the patient's preferred pharmacies. - `patient_id` and `pharmacies` are required, and `patient_id` must be the id of an existing patient. - At most one pharmacy in `pharmacies` can be marked as the default. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `patient_id`| `str` or `UUID`| The id of the [patient](/sdk/data-patient/).| Yes `pharmacies`| list[PatientPreferredPharmacy]| List of pharmacies to create.| Yes ## PatientPreferredPharmacy The `PatientPreferredPharmacy` dataclass represents a patient's preferred pharmacy, and if it's their default pharmacy. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `ncpdp_id`| `str`| The NCPDC identifier of the pharmacy.| Yes `default`| `bool`| Indicates if this is the patient's default pharmacy. Defaults to `False`.| No ### Validation When this effect is interpreted, Canvas validates the `ncpdp_id` before setting the preferred pharmacy. If the `ncpdp_id` is invalid or does not exist, the effect will fail. To ensure the `ncpdp_id` exists before using this effect, you can verify it using Canvas's [pharmacy HTTP utility](/sdk/utils/#making-requests-to-the-pharmacy-service) to check the pharmacy beforehand. ## Example ```python from canvas_sdk.effects.patient import CreatePatientPreferredPharmacies, PatientPreferredPharmacy from canvas_sdk.v1.data import Patient as PatientModel first_patient_id = PatientModel.objects.values_list("id", flat=True).first() preferred_pharmacies_effect = CreatePatientPreferredPharmacies( pharmacies=[PatientPreferredPharmacy(ncpdp_id="0586163", default=True)], patient_id=first_patient_id ) preferred_pharmacies_effect.create() ``` This effect will create a new preferred pharmacy for the specified patient. Since the `default` attribute is set to `True`, it will mark this pharmacy as the patient's default preferred pharmacy. --- # Data Integration Source: https://docs.canvasmedical.com/sdk/effect-data-integration/ Plugins can automate triage of inbound clinical documents in the [Data Integration queue](/sdk/data-integration-task/) — lab reports, imaging reports, faxes, clinical and administrative documents, and other uploaded files awaiting staff review before they're attached to a patient's chart. Most of these effects work by writing a **prefill suggestion** to the IntegrationTask. The Data Integration UI surfaces that suggestion as a pre-populated value in the staff member's review form — the suggested patient, document type, template values, or reviewer assignment — along with optional annotation badges. A staff member still reviews and commits the change; the plugin doesn't directly mutate the IntegrationTask. The exceptions are `JunkDocument` and `RemoveDocumentFromPatient`, which act on the IntegrationTask's status or patient link immediately without writing a prefill. ## Assigning a Reviewer To assign a staff member or team as the reviewer for a document in the Data Integration queue, build an `AssignDocumentReviewer` and return its `apply()` from your handler. ### Methods #### apply() → Effect Suggests the reviewer for the document. - `document_id` is required. - Set `reviewer_id` or `team_id`. If both are set, the staff reviewer is used; with neither, the effect does nothing. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `document_id`| `str`| The `id` of the [IntegrationTask](/sdk/data-integration-task/#integrationtask) document.| Yes `reviewer_id`| `str`| The `id` of the [Staff](/sdk/data-staff/#staff) member to assign as reviewer.| One of `reviewer_id` or `team_id` `team_id`| `str`| The `id` of the [Team](/sdk/data-team/#team) to assign as reviewer.| One of `reviewer_id` or `team_id` `review_mode`| ReviewMode| Review mode. Defaults to `ReviewMode.REVIEW_REQUIRED`.| No `annotations`| `list`| List of annotations for display in the UI. See Annotations.| No ### ReviewMode Value| Description ---|--- `ReviewMode.REVIEW_REQUIRED` (default)| Document requires explicit review. `ReviewMode.ALREADY_REVIEWED`| Document is marked as already reviewed. `ReviewMode.REVIEW_NOT_REQUIRED`| Document does not require review. An example of assigning a staff reviewer. Annotations render as colored badges next to the reviewer field in the Data Integration UI — useful for surfacing why the plugin chose this reviewer: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.data_integration import AssignDocumentReviewer, ReviewMode from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class AssignReviewerHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.DOCUMENT_RECEIVED) def compute(self) -> list[Effect]: return [ AssignDocumentReviewer( document_id=self.event.target.id, reviewer_id="4150cd20de8a470aa570a852859ac87e", review_mode=ReviewMode.ALREADY_REVIEWED, annotations=[ {"text": "Team lead", "color": "#4CAF50"}, {"text": "Auto-assigned", "color": "#FF9800"}, ], ).apply() ] ``` An example of assigning a team instead of an individual reviewer: ```python from canvas_sdk.effects.data_integration import AssignDocumentReviewer assign_reviewer = AssignDocumentReviewer( document_id="d2194110-5c9a-4842-8733-ef09ea5ead11", team_id="3f8a2e1c-9b4d-4f5a-8c7e-1d2b3a4c5d6e", annotations=[ {"text": "Routed to Coding team", "color": "#2196F3"}, ], ) ``` ## Categorizing a Document To categorize a document in the Data Integration queue into a specific document type, build a `CategorizeDocument` and return its `apply()` from your handler. ### Methods #### apply() → Effect Suggests the document type. - `document_id` and `document_type` are required. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `document_id`| `str`| The `id` of the [IntegrationTask](/sdk/data-integration-task/#integrationtask) document to categorize.| Yes `document_type`| DocumentType| Document type information for categorizing the document.| Yes `annotations`| `list`| List of annotations for display in the UI. See Annotations.| No ### DocumentType The `document_type` parameter is a dictionary with the following fields: Key| | Type| Description ---|---|---|--- `key`| required| string| The unique key identifying the document type. Must match a key from the Supported Document Types table below. `name`| required| string| The human-readable name of the document type. Should match the catalog's `Name` for that key. `report_type`| required| string| The type of report. Must be `"CLINICAL"` or `"ADMINISTRATIVE"`. `template_type`| required| string | null| Must be `"LabReportTemplate"`, `"ImagingReportTemplate"`, `"SpecialtyReportTemplate"`, or `null`. ### Supported Document Types Canvas's built-in document type catalog. Use the `Key` value in your `document_type` dict; the other columns show the catalog's `report_type` and the matching parse template (when applicable). Name| Key| Report Type| Template Type ---|---|---|--- Advance Beneficiary Notice| `5375e6ae238e41b4972717174be99d10`| ADMINISTRATIVE| `null` Advance Directive / Living Will| `1d5a4821140dab935e90d9d73bfd7a35`| ADMINISTRATIVE| `null` CDL (Commercial Driver License)| `7639c58fb4b75e2ff74270787eda80a7`| ADMINISTRATIVE| `null` Care Management| `6b4b539a233145fe871e8ac703f39fcb`| CLINICAL| `null` Disability Form| `a5fd8d81026747c0b01f757b7935f82a`| ADMINISTRATIVE| `null` Emergency Department Report| `67037fd377654984b8b368b47d0ab0e4`| CLINICAL| `null` External Medical Records| `b61d0a4ebf4316a1a3beea32bec88052`| CLINICAL| `null` Handicap Parking Permit| `649a852657357c20491856f4eb7a2690`| ADMINISTRATIVE| `null` Home Care Report| `d368eaa8f1b2419cb792bb7876bfac8a`| CLINICAL| `null` Hospital Discharge Summary| `6be998e1335a4d9689cae33ec7ed6968`| CLINICAL| `null` Hospital History & Physical| `d9060893790744589c5252ddb81b785e`| CLINICAL| `null` Imaging Report| `87041869c5954337b84fd10094fe5c0a`| CLINICAL| `ImagingReportTemplate` In-Office Testing| `372e7248ba944dbeab54712078c0ec44`| CLINICAL| `null` Insurance Card| `2551841bcfd34e1aa839cb1e3b7ef48f`| ADMINISTRATIVE| `null` Insurer Prior Authorization| `0394e7a3a5c847f495414e7511d12543`| ADMINISTRATIVE| `null` Lab Report| `f605e084dcad4beca16c0f62e6586d76`| CLINICAL| `LabReportTemplate` Medicaid Documents| `e02e0f6dc76d42aaa61384c85ca90830`| ADMINISTRATIVE| `null` Nursing Home| `fc14824cdc9fdcd1e2ce005aa3019d20`| CLINICAL| `null` Operative Report| `7f118206607248b7b13409e69c638eba`| CLINICAL| `null` POLST (Provider Order for Life Sustaining Treatment)| `cf72e522dd95fa1da1297cf3bf5e54e8`| ADMINISTRATIVE| `null` Patient Administrative Intake Form| `b25601a3e8ac543f5f2d7a85006de223`| ADMINISTRATIVE| `null` Patient Agreement| `2e16ccd7ad5a4bcf9d21dd51b4d16cb9`| ADMINISTRATIVE| `null` Patient Assistance| `ebd8c4f6f35b4c008e512d0e3c666e95`| ADMINISTRATIVE| `null` Patient Clinical Intake Form| `8c9ca86c76704d57a775cd6f48a02b6c`| CLINICAL| `null` Patient Consent| `7ce5a6eefedcff89a5a460f6be89d308`| ADMINISTRATIVE| `null` Physical Exams| `b1146b76cd2c4b488c964cf497fb1dce`| CLINICAL| `null` Power of Attorney| `1ff5f640868528e633a5d45f2142161e`| ADMINISTRATIVE| `null` Prescription Card| `0d002c5fe86c44b0a63c08e70e0df37c`| ADMINISTRATIVE| `null` Prescription Refill Request| `714a8229339b4af3989403a308bdcbfb`| CLINICAL| `null` Rehabilitation Report| `660f6fce32a64b3f9817f4bad3d56c79`| CLINICAL| `null` Release of Information Request| `2283372cb0fa4962a6fcff2eb3ca080b`| ADMINISTRATIVE| `null` Specialist Consult Report| `f0f1398f6f4640d29e4ff80d5481eb3f`| CLINICAL| `SpecialtyReportTemplate` Uncategorized Administrative Document| `7ebe754f4c3b860cf80d3aa9ebd8494c`| ADMINISTRATIVE| `null` Uncategorized Clinical Document| `52ef59487ecabc9cdd645c21c7a35458`| CLINICAL| `null` Worker's Compensation Documents| `54a06a3f06b48cbebf8dec88707272c3`| ADMINISTRATIVE| `null` ### Example The `DOCUMENT_RECEIVED` event context includes `available_document_types`, a list of every document type the instance supports — each entry carries the `key`, `name`, `report_type`, and `template_type` you'll need to construct a `CategorizeDocument` effect. The idiomatic pattern is to read from that list rather than hardcode catalog values, so a plugin keeps working as the catalog changes. The handler below matches by `name` (using the Supported Document Types table above as a reference for what names to expect) and forwards the matched values to the effect. `ReportType` and `TemplateType` enum instances are constructed explicitly from the context's string values — the SDK rejects raw strings. ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.data_integration import CategorizeDocument from canvas_sdk.effects.data_integration.types import ReportType, TemplateType from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class CategorizeLabReports(BaseHandler): RESPONDS_TO = EventType.Name(EventType.DOCUMENT_RECEIVED) def compute(self) -> list[Effect]: available = self.event.context.get("available_document_types", []) lab_report = next((dt for dt in available if dt["name"] == "Lab Report"), None) if not lab_report: return [] template_type = lab_report.get("template_type") return [ CategorizeDocument( document_id=self.event.target.id, document_type={ "key": lab_report["key"], "name": lab_report["name"], "report_type": ReportType(lab_report["report_type"]), "template_type": TemplateType(template_type) if template_type else None, }, annotations=[ {"text": "AI Categorized", "color": "#4CAF50"}, ], ).apply() ] ``` For full end-to-end usage including discovery, error handling, and the other Data Integration effects in a single handler, see the [`data_integration_example` plugin](https://github.com/canvas-medical/canvas-plugins/tree/main/example-plugins/data_integration_example) in canvas-plugins. ## Linking a Document to a Patient To link a document in the Data Integration queue to a patient, build a `LinkDocumentToPatient` and return its `apply()` from your handler. The plugin is responsible for matching the patient and supplying their id; Canvas does not search for matching patients itself. ### Methods #### apply() → Effect Suggests the patient to link the document to. - `document_id` and `patient_key` are required. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `document_id`| `str`| The `id` of the [IntegrationTask](/sdk/data-integration-task/#integrationtask) document.| Yes `patient_key`| `str`| The `id` of the [Patient](/sdk/data-patient/#patient) to link the document to.| Yes `annotations`| `list`| List of annotations for display in the UI. See Annotations.| No An example of linking a document to a patient: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.data_integration import LinkDocumentToPatient from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data.patient import Patient class LinkDocumentHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.DOCUMENT_RECEIVED) def compute(self) -> list[Effect]: document_id = self.event.target.id document_title = self.event.context.get("document", {}).get("title", "") # Plugin-specific matching logic — e.g., parse the document title or # call an OCR/LLM service to extract patient demographics, then look # up the patient via the SDK data module. patient = Patient.objects.filter(...).first() if not patient: return [] return [ LinkDocumentToPatient( document_id=document_id, patient_key=patient.id, annotations=[ {"text": "AI 92%", "color": "#4CAF50"}, {"text": f"Matched from '{document_title}'", "color": "#2196F3"}, ], ).apply() ] ``` ## Marking a Document as Junk To mark a document in the Data Integration queue as junk (spam), build a `JunkDocument` and return its `apply()` from your handler. ### Methods #### apply() → Effect Marks the document as junk immediately. - `document_id` is required, and must be the id of an existing IntegrationTask in an early-stage state. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `document_id`| `str`| The `id` of the [IntegrationTask](/sdk/data-integration-task/#integrationtask) document to mark as junk.| Yes `JunkDocument` only works on IntegrationTasks in early-stage states: **Unread** , **Read** , **Error** , **Unread error** , or **Junk** (already). IntegrationTasks in **Processed** or **Reviewed** states cannot be junked — attempting to do so raises a validation error. The effect also validates that `document_id` resolves to an existing IntegrationTask and is a well-formed UUID; missing, malformed, or unknown IDs raise validation errors before the IntegrationTask is touched. To avoid the validation error for tasks that have moved past the early-stage states, you can read the [`status`](/sdk/data-integration-task/#integrationtask) field from the SDK data module and preflight-check before emitting the effect. An example that preflight-checks the IntegrationTask status before junking: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.data_integration import JunkDocument from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data.integration_task import IntegrationTask, IntegrationTaskStatus class JunkDocumentHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.DOCUMENT_RECEIVED) def compute(self) -> list[Effect]: document_id = self.event.target.id task = IntegrationTask.objects.get(id=document_id) # Skip tasks that have already been processed or reviewed if task.status in (IntegrationTaskStatus.PROCESSED, IntegrationTaskStatus.REVIEWED): return [] return [JunkDocument(document_id=document_id).apply()] ``` ## Prefilling Document Fields To pre-populate the parse-template fields on an IntegrationTask, build a `PrefillDocumentFields` and return its `apply()` from your handler. **Only three document types support field prefill** — those whose `template_type` is a known parse-template family: Document type| `template_type`| SDK data module ---|---|--- Lab Report| `LabReportTemplate`| [`LabReportTemplate`](/sdk/data-lab-report-template/) Imaging Report| `ImagingReportTemplate`| [`ImagingReportTemplate`](/sdk/data-imaging-report-template/) Specialist Consult Report| `SpecialtyReportTemplate`| [`SpecialtyReportTemplate`](/sdk/data-specialty-report-template/) Document types with inline fields (Patient Consent, Power of Attorney, the Uncategorized variants, and the other ~30 entries in the Supported Document Types catalog) cannot have their fields prefilled by this effect. ### Methods #### apply() → Effect Suggests values for the template fields. - `document_id` and at least one entry in `templates` are required. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `document_id`| `str`| The `id` of the [IntegrationTask](/sdk/data-integration-task/#integrationtask) document.| Yes `templates`| list[PrefillTemplate]| One or more templates to prefill. Must contain at least one entry.| Yes `annotations`| `list`| List of annotations for display in the UI. See Annotations.| No ### PrefillTemplate A `PrefillTemplate` is a dictionary with the following keys: Key| | Type| Description ---|---|---|--- `template_id`| required| int| The integer `dbid` of a `LabReportTemplate`, `ImagingReportTemplate`, or `SpecialtyReportTemplate` record. (Note: this is the integer primary key, not the `id` UUID.) Look this up via the SDK data module — see the example below. `template_name`| required| string| The matching template's `.name`. `fields`| required| dict[str, PrefillDocumentFieldData]| Map of field name to field data. Keys must match the `label` of a field on the chosen template — iterate `template.fields.all()` to discover the available labels, units, types, and required-ness. ### PrefillDocumentFieldData A `PrefillDocumentFieldData` is a dictionary with the following keys: Key| | Type| Description ---|---|---|--- `value`| required| string| The field value, stringified into the form field at render time. The same rules apply to all three template families (`LabReportTemplateField`, `ImagingReportTemplateField`, `SpecialtyReportTemplateField`). Currently supported are fields whose `type` is `"float"`, `"text"`, `"date"`, `"select"`, or `"radio"`. For `"select"` and `"radio"`, the value must exactly match one of the field's `options[*].key` for the dropdown to pre-select. `unit`| optional| string| Unit of measurement. Should match the corresponding template field's `units` (`LabReportTemplateField.units`, `ImagingReportTemplateField.units`, or `SpecialtyReportTemplateField.units`). `reference_range`| optional| string| Reference range for the value. `abnormal`| optional| bool| Whether the value is abnormal. `annotations`| optional| list| Per-field annotations. Same shape as Annotations. ### Discovering Available Fields Before constructing a `PrefillDocumentFields` payload, you can inspect the template to see what labels, units, types, and option keys it defines. The same approach works for `ImagingReportTemplate` and `SpecialtyReportTemplate`. ```python from canvas_sdk.v1.data import LabReportTemplate from logger import log def inspect_lab_template(template_name) -> None: template = ( LabReportTemplate.objects .active() .filter(name=template_name) .first() ) if not template: log.info(f"No template found with name {template_name}") return log.info(f"Template: {template.name} (dbid={template.dbid})") for field in template.fields.all().order_by("sequence"): log.info( f" {field.label}", type=field.type, units=field.units, required=field.required, code=field.code, code_system=field.code_system, ) for opt in field.options.all(): log.info(f" option", key=opt.key, label=opt.label) ``` Sample output for a CBC Panel template: ```text Template: CBC Panel (dbid=42) Hemoglobin type=float units=g/dL required=True code=718-7 code_system=LOINC WBC type=float units=10^3/uL required=True code=6690-2 code_system=LOINC Differential type=select units= required=False code= code_system= option key=NORMAL label=Normal option key=ABNORMAL label=Abnormal ``` With that you can see which labels go into the `fields` dict's keys, what `unit` value to send, and — for `select`/`radio` fields — which `option.key` values are valid for the `value` field. ### Example An example of prefilling a Lab Report. The plugin looks up the CBC Panel template via the [`LabReportTemplate`](/sdk/data-lab-report-template/) SDK data module, then iterates the template's `fields` relation to discover which labels and units are available before filling in the values it extracted from the document. ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.data_integration import PrefillDocumentFields from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data import LabReportTemplate class PrefillLabReportHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.DOCUMENT_RECEIVED) def compute(self) -> list[Effect]: cbc = ( LabReportTemplate.objects .active() .filter(name__icontains="CBC") .first() ) if not cbc: return [] # Values the plugin extracted from the document via OCR/LLM. # Hardcoded here for clarity. extracted = { "Hemoglobin": ("13.5", False), "WBC": ("11.2", True), } # Iterate the template's fields to discover which labels and units # the template defines, then fill in only the ones the plugin has # values for. Each field exposes `label`, `units`, `type`, and # `required` (see LabReportTemplateField). prefill_fields: dict[str, dict] = {} for field in cbc.fields.all(): if field.label not in extracted: continue value, abnormal = extracted[field.label] prefill_fields[field.label] = { "value": value, "unit": field.units or "", "abnormal": abnormal, } return [ PrefillDocumentFields( document_id=self.event.target.id, templates=[ { "template_id": cbc.dbid, "template_name": cbc.name, "fields": prefill_fields, }, ], annotations=[ {"text": "Prefilled via AI", "color": "#FF9800"}, ], ).apply() ] ``` ## Removing a Document from a Patient To unlink a document from its currently-linked patient in the Data Integration queue, build a `RemoveDocumentFromPatient` and return its `apply()` from your handler. An IntegrationTask carries at most one patient link at a time, so this effect simply clears that link. ### Methods #### apply() → Effect Clears the document's patient link immediately. - `document_id` is required. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `document_id`| `str`| The `id` of the [IntegrationTask](/sdk/data-integration-task/#integrationtask) document to unlink from its patient.| Yes `patient_id`| `str`| The id of the [patient](/sdk/data-patient/) the document is linked to. Recorded in the log for auditing; the link is cleared whatever its value.| No An example of unlinking a document from its patient: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.data_integration import RemoveDocumentFromPatient from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class RemoveDocumentHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.DOCUMENT_LINKED_TO_PATIENT) def compute(self) -> list[Effect]: return [ RemoveDocumentFromPatient( document_id=self.event.target.id, ).apply() ] ``` ## Annotations The `annotations` field on any data integration effect accepts a list of dictionaries with the following keys: Key| Type| Description ---|---|--- `text`| string| The annotation text to display (e.g., "AI 95%"). `color`| string| Hex color code (e.g., "#4CAF50" for green). --- # EventValidationError Effect Source: https://docs.canvasmedical.com/sdk/effect-event-validation-error/ ## Overview The `EventValidationError` effect is used to block the creation of an event (such as a NoteStateChangeEvent create) when custom validation fails. If this effect is returned by a protocol in response to an event (e.g., `NOTE_STATE_CHANGE_EVENT_PRE_CREATE`), the event is aborted and the provided error message is surfaced to the user. Build an `EventValidationError`, add one or more errors with `add_error()`, and return its `apply()` from your handler. ## Methods ### add_error(message: str | ValidationError) → EventValidationError Adds a validation error to the effect, either as a message string or a `ValidationError`. It returns the effect, so calls can be chained. ### apply() → Effect Blocks the event and shows the errors to the user. ## Attributes Attribute| Type| Description| Required ---|---|---|--- errors| list[ValidationError]| List of validation errors to display to the user.| Yes ### ValidationError dataclass Each item in the `errors` list is a `ValidationError` dataclass with the following fields: Field| Type| Description ---|---|--- message| string| The error message to display to the user. ## Example Usage Return an `EventValidationError` from your protocol's `compute` method to block the event and show a message to the user. You can also return other effects alongside `EventValidationError`. ```python from canvas_sdk.effects import Effect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data import Note from canvas_sdk.v1.data.coverage import CoverageStack from canvas_sdk.effects.validation import EventValidationError, ValidationError from canvas_sdk.effects.banner_alert import AddBannerAlert, RemoveBannerAlert class MyHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_PRE_CREATE) def handle_no_coverage(self, patient_id: str, note: Note) -> list[Effect]: """If the patient has no coverage, add banner alert and do not allow notes to be locked or charges pushed.""" if note.patient.coverages.filter(stack=CoverageStack.IN_USE).count() == 0: return [ EventValidationError( errors=[ ValidationError( message="Patient has no coverage. Do not send claim to billing department until coverage(s) have been added." ) ] ).apply(), AddBannerAlert( patient_id=patient_id, key="no_coverage", narrative="Patient has no documented coverages.", placement=[ AddBannerAlert.Placement.CHART, AddBannerAlert.Placement.APPOINTMENT_CARD, ], intent=AddBannerAlert.Intent.ALERT, ).apply(), ] return [RemoveBannerAlert(patient_id=patient_id, key="no_coverage").apply()] def handle_no_billing_line_items(self, note: Note) -> Effect | None: """If the note has no billing line items, do not allow notes to be locked or charges pushed.""" if note.billing_line_items.filter(status="active").count() > 0: return None val_effect = EventValidationError() val_effect.add_error( "Cannot lock or push charges for a note with no billing line items." ) return val_effect.apply() def compute(self) -> list[Effect]: state = self.event.context["state"] if state not in ["PSH", "LKD"]: return [] note = Note.objects.get(id=self.event.context["note_id"]) patient_id = str(note.patient.id) effects = self.handle_no_coverage(patient_id=patient_id, note=note) if v := self.handle_no_billing_line_items(note=note): effects.append(v) return effects ``` ## Supported Events Event| Behavior ---|--- [`NOTE_STATE_CHANGE_EVENT_PRE_CREATE`](/sdk/events/#notes)| Blocks the note state change (e.g. lock, push charges) and displays errors in the UI. [`APPOINTMENT__FORM__UPDATED`](/sdk/events/#appointments)| Disables the Book button on the appointment scheduling modal and displays errors as a tooltip. ## Implementation Details - If an `EventValidationError` is returned, the event is aborted and the error message is shown in the UI (if initiated from the UI). - This effect is typically used for pre-create validation of events, such as note state changes or appointment scheduling. - Any other effects returned alongside an `EventValidationError` are still applied, even though the event itself is blocked. In the example above, the `AddBannerAlert` effect is added and persists even when the note state change is rejected. --- # External Event Effect Source: https://docs.canvasmedical.com/sdk/effect-external-event/ The `ExternalEvent` effect provides a way to create and update external clinical events within the Canvas platform. External events represent clinical encounters from external data sources such as ADT (Admission, Discharge, Transfer) feeds, enabling tracking of patient visits that occur outside of Canvas. Build an `ExternalEvent` with the attributes you want to set, then return one of its two methods from your handler: `create()` for a new external event, or `update()` for an existing one. ## Methods ### create() → Effect Creates an external event. - `patient_id`, `visit_identifier`, `message_control_id`, and `event_type` are required. - `external_event_id` must not be set. ### update() → Effect Updates an existing external event. - `external_event_id` is required, and must be the id of an existing external event. - Only the attributes you set on the effect are changed. Attributes you leave unset keep their current values. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `external_event_id`| `str` or `UUID` or `None`| Unique identifier of an existing external event. Must be unset when creating.| For `update()` `patient_id`| `str` or `None`| ID of the patient for this event.| For `create()` `visit_identifier`| `str` or `None`| Identifier for the visit/encounter.| For `create()` `message_control_id`| `str` or `None`| Unique identifier for the message (e.g., HL7 message control ID).| For `create()` `event_type`| `str` or `None`| Type of event (e.g., "ADT^A01" for admission).| For `create()` `event_datetime`| `datetime` or `None`| Date and time when the event occurred.| No `event_cancelation_datetime`| `datetime` or `None`| Date and time when the event was cancelled. Set this to mark an event as cancelled.| No `message_datetime`| `datetime` or `None`| Date and time when the message was sent.| No `information_source`| `str` or `None`| Source of the event information (e.g., hospital name, system name).| No `facility_name`| `str` or `None`| Name of the facility where the event occurred.| No `raw_message`| `str` or `None`| Raw message content (e.g., original HL7 message).| No ## Validation & Errors Before any effect is emitted, the model runs these checks: ### Create Validation - **external_event_id** must **not** be set (will be generated by the system) - **patient_id** is **required** - **visit_identifier** is **required** - **message_control_id** is **required** - **event_type** is **required** ### Update Validation - **external_event_id** is **required** and must reference an existing external event - All other fields are optional; only dirty (modified) fields are updated ## Example Usage ```python from datetime import datetime from canvas_sdk.effects.external_event import ExternalEvent from canvas_sdk.v1.data.external_event import ExternalEvent as ExternalEventModel from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.first() ``` ### Create an Admission Event ```python # Create an external event for a hospital admission admission_event = ExternalEvent( patient_id=str(patient.id), visit_identifier="VISIT-2024-001234", message_control_id="MSG-20240115-143052", event_type="ADT^A01", # Admission event_datetime=datetime.now(), message_datetime=datetime.now(), information_source="General Hospital ADT Feed", facility_name="General Hospital - Main Campus", raw_message="MSH|^~\&|HOSPITAL|FAC|CANVAS|...", ) effect_create = admission_event.create() ``` ### Create a Discharge Event ```python # Create an external event for a discharge discharge_event = ExternalEvent( patient_id=str(patient.id), visit_identifier="VISIT-2024-001234", # Same visit as admission message_control_id="MSG-20240118-091530", event_type="ADT^A03", # Discharge event_datetime=datetime.now(), message_datetime=datetime.now(), information_source="General Hospital ADT Feed", facility_name="General Hospital - Main Campus", ) effect_discharge = discharge_event.create() ``` ### Cancel an Existing Event ```python # Find an existing external event to cancel existing_event = ExternalEventModel.objects.filter( patient__id=patient.id, event_cancelation_datetime__isnull=True, # Not already cancelled ).first() if existing_event: # Cancel the event by setting the cancelation datetime cancel_effect = ExternalEvent( external_event_id=str(existing_event.id), event_cancelation_datetime=datetime.now(), ) effect_cancel = cancel_effect.update() ``` ### Update Event Details ```python # Update an existing external event with additional information existing_event = ExternalEventModel.objects.filter(patient__id=patient.id).first() if existing_event: updated_event = ExternalEvent( external_event_id=str(existing_event.id), facility_name="General Hospital - West Wing (Corrected)", raw_message="MSH|^~\&|HOSPITAL|FAC|CANVAS|...|CORRECTED", ) effect_update = updated_event.update() ``` ### Create Event with All Fields ```python # Create an external event with all optional fields populated complete_event = ExternalEvent( # Required fields patient_id=str(patient.id), visit_identifier="VISIT-2024-005678", message_control_id="MSG-20240120-163045", event_type="ADT^A01", # Optional datetime fields event_datetime=datetime(2024, 1, 20, 16, 30, 0), message_datetime=datetime(2024, 1, 20, 16, 30, 45), # Optional string fields information_source="Regional Medical Center - HL7 Interface", facility_name="Regional Medical Center - Emergency Department", raw_message="MSH|^~\&|RMC|ED|CANVAS|RECV|20240120163045||ADT^A01|MSG123|P|2.5", ) effect_complete = complete_event.create() ``` ## Common Event Types The `event_type` field typically contains HL7 ADT event codes: Event Type| Description ---|--- ADT^A01| Admit/Visit Notification ADT^A02| Transfer a Patient ADT^A03| Discharge/End Visit ADT^A04| Register a Patient ADT^A08| Update Patient Information ADT^A11| Cancel Admit/Visit Notification ADT^A12| Cancel Transfer ADT^A13| Cancel Discharge/End Visit --- # Fax Effects Source: https://docs.canvasmedical.com/sdk/effect-fax/ The Canvas SDK includes effects to fax a note, or to fax a referral, imaging order, lab order or letter without the rest of its note. The recipient gets the same PDF that the document's own **Fax** action sends from Canvas. Effect| Sends| Recipient when you don't name one ---|---|--- `FaxNoteEffect`| The note| None. A recipient is required. `FaxReferralEffect`| The referral and its attachments, plus the visit note when the referral includes it| The referral's service provider, when it has a fax number `FaxImagingOrderEffect`| The imaging order| The order's imaging center, when it has a fax number `FaxLabOrderEffect`| The lab order's requisition form| None. A recipient is required. `FaxLetterEffect`| The letter and its attachments| None. A recipient is required. ## Fax a Note To fax a note, build a `FaxNoteEffect` and return its `apply()` from your handler. This effect is also documented under [Fax Note](/sdk/effect-notes/#fax-note) on the Note Effects page. ### Methods #### apply() → Effect Faxes the note. - Validates that the note exists. - If `include_coversheet` is `True`, `subject`, `comment` and `location_id` become required, and the practice location must exist. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `note_id`| `UUID` or `str`| Identifier of the note to fax.| Yes `recipient_name`| `str`| Name of the fax recipient.| Yes `recipient_fax_number`| `str`| Fax number of the recipient, including the country code.| Yes `include_coversheet`| `bool`| Whether to include a coversheet with the fax.| No `subject`| `str` or `None`| Subject line for the coversheet (required if coversheet used).| No `comment`| `str` or `None`| Additional comments for the coversheet (required if coversheet used).| No `location_id`| `UUID` or `str` or `None`| Practice location ID (required if coversheet used).| No ### Example ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.fax import FaxNoteEffect from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self) -> list[Effect]: fax_effect = FaxNoteEffect( note_id="existing-note-uuid", recipient_name="Dr. Jane Smith", recipient_fax_number="15551234567", include_coversheet=True, subject="Patient Referral - Follow-up Care", comment="Please review attached consultation notes for continuing care.", location_id="practice-location-uuid", ) return [fax_effect.apply()] ``` ## Fax a Referral To fax a referral, build a `FaxReferralEffect` and return its `apply()` from your handler. The fax includes the referral's attachments, and the visit note when the referral includes it. ### Methods #### apply() → Effect Faxes the referral. - Validates that the referral exists. It must be committed and not entered in error, as for its own **Fax** action in Canvas. - Leave out both `recipient_name` and `recipient_fax_number` to fax the referral's service provider. Nothing is faxed if you give only one, if the referral has no service provider, or if the service provider has no fax number. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `referral_id`| `UUID` or `str`| Identifier of the referral to fax.| Yes `recipient_name`| `str` or `None`| Name of the fax recipient. Give it together with `recipient_fax_number`.| No `recipient_fax_number`| `str` or `None`| Fax number of the recipient, including the country code. Give it together with `recipient_name`.| No `include_coversheet`| `bool`| Whether to include a coversheet with the fax.| No `subject`| `str` or `None`| Subject line for the coversheet (required if coversheet used).| No `comment`| `str` or `None`| Additional comments for the coversheet (required if coversheet used).| No `location_id`| `UUID` or `str` or `None`| Practice location ID (required if coversheet used).| No ### Example Fax each referral to its service provider when its Refer command is committed: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.fax import FaxReferralEffect from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler from canvas_sdk.v1.data import Command class FaxReferralOnCommit(BaseHandler): RESPONDS_TO = EventType.Name(EventType.REFER_COMMAND__POST_COMMIT) def compute(self) -> list[Effect]: referral = Command.objects.get(id=self.event.target.id).anchor_object if referral is None: return [] return [FaxReferralEffect(referral_id=referral.id).apply()] ``` ## Fax an Imaging Order To fax an imaging order, build a `FaxImagingOrderEffect` and return its `apply()` from your handler. ### Methods #### apply() → Effect Faxes the imaging order. - Validates that the imaging order exists. It must be committed and not entered in error, as for its own **Fax** action in Canvas. - Leave out both `recipient_name` and `recipient_fax_number` to fax the order's imaging center. Nothing is faxed if you give only one, if the order has no imaging center, or if the imaging center has no fax number. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `imaging_order_id`| `UUID` or `str`| Identifier of the imaging order to fax.| Yes `recipient_name`| `str` or `None`| Name of the fax recipient. Give it together with `recipient_fax_number`.| No `recipient_fax_number`| `str` or `None`| Fax number of the recipient, including the country code. Give it together with `recipient_name`.| No `include_coversheet`| `bool`| Whether to include a coversheet with the fax.| No `subject`| `str` or `None`| Subject line for the coversheet (required if coversheet used).| No `comment`| `str` or `None`| Additional comments for the coversheet (required if coversheet used).| No `location_id`| `UUID` or `str` or `None`| Practice location ID (required if coversheet used).| No ### Example Fax an imaging order to its imaging center: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.fax import FaxImagingOrderEffect from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self) -> list[Effect]: return [FaxImagingOrderEffect(imaging_order_id="existing-imaging-order-uuid").apply()] ``` ## Fax a Lab Order To fax a lab order's requisition form, build a `FaxLabOrderEffect` and return its `apply()` from your handler. ### Methods #### apply() → Effect Faxes the lab order's requisition form. - Validates that the lab order exists. It must be committed and not entered in error, as for its own **Fax** action in Canvas. - `recipient_name` and `recipient_fax_number` are required, because a lab order has no recipient on file. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `lab_order_id`| `UUID` or `str`| Identifier of the lab order to fax.| Yes `recipient_name`| `str`| Name of the fax recipient.| Yes `recipient_fax_number`| `str`| Fax number of the recipient, including the country code.| Yes `include_coversheet`| `bool`| Whether to include a coversheet with the fax.| No `subject`| `str` or `None`| Subject line for the coversheet (required if coversheet used).| No `comment`| `str` or `None`| Additional comments for the coversheet (required if coversheet used).| No `location_id`| `UUID` or `str` or `None`| Practice location ID (required if coversheet used).| No ### Example Fax a lab order's requisition form to the lab, with a coversheet: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.fax import FaxLabOrderEffect from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self) -> list[Effect]: fax_effect = FaxLabOrderEffect( lab_order_id="existing-lab-order-uuid", recipient_name="Acme Labs", recipient_fax_number="15555550100", include_coversheet=True, subject="Lab requisition", comment="Requisition for the attached lab order.", location_id="practice-location-uuid", ) return [fax_effect.apply()] ``` ## Fax a Letter To fax a letter, build a `FaxLetterEffect` and return its `apply()` from your handler. The fax includes the letter's attachments. ### Methods #### apply() → Effect Faxes the letter. - Validates that the letter exists. - `recipient_name` and `recipient_fax_number` are required. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `letter_id`| `UUID` or `str`| Identifier of the letter to fax.| Yes `recipient_name`| `str`| Name of the fax recipient.| Yes `recipient_fax_number`| `str`| Fax number of the recipient, including the country code.| Yes `include_coversheet`| `bool`| Whether to include a coversheet with the fax.| No `subject`| `str` or `None`| Subject line for the coversheet (required if coversheet used).| No `comment`| `str` or `None`| Additional comments for the coversheet (required if coversheet used).| No `location_id`| `UUID` or `str` or `None`| Practice location ID (required if coversheet used).| No ### Example ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.fax import FaxLetterEffect from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self) -> list[Effect]: fax_effect = FaxLetterEffect( letter_id="existing-letter-uuid", recipient_name="Dr. Jane Smith", recipient_fax_number="15551234567", ) return [fax_effect.apply()] ``` ## Fax history Each fax is recorded on its document with Canvas Bot as its `originator`, and appears in the document's fax history in Canvas. `delivered_by_fax` is updated when the faxing service reports the outcome. See [Faxes sent by a plugin](/sdk/data-fax/#faxes-sent-by-a-plugin). Effect| Recorded as ---|--- `FaxNoteEffect`| [NoteActionEvent](/sdk/data-fax/#noteactionevent), read through `note.action_events` `FaxReferralEffect`| [ReferralActionEvent](/sdk/data-fax/#referralactionevent) `FaxImagingOrderEffect`| [ImagingOrderActionEvent](/sdk/data-fax/#imagingorderactionevent) `FaxLabOrderEffect`| [LabOrderActionEvent](/sdk/data-fax/#laborderactionevent) `FaxLetterEffect`| [LetterActionEvent](/sdk/data-letter-action-event/) --- # HTTP Request Source: https://docs.canvasmedical.com/sdk/effect-http-request/ The `HttpRequestEffect` lets a plugin ask the Canvas platform to issue an HTTP request on its behalf. The plugin returns the effect from a handler and the platform performs the call. We recommend running the request asynchronously by chaining `.set_async(...)` onto the applied effect. The request is then handed off to the platform's async runner, which manages delay, retries, and backoff so the handler doesn't block on the network. Setting `retry_on_status_codes` automatically opts into async execution (equivalent to `.set_async(delay_seconds=0)`), so you only need to call `.set_async(...)` explicitly to add a delay or set `max_retries`. Without `.set_async(...)` and without `retry_on_status_codes`, the effect is executed inline. Build an `HttpRequestEffect` and return its `apply()` from your handler. ## Methods ### apply() → Effect Sends the HTTP request. - `url` is required. - `method` defaults to `GET`. ## Attributes Name| Type| Description| Required ---|---|---|--- `url`| `str`| The URL to request. Must be non-empty. Cannot resolve to a private or loopback address (see Security).| Yes `method`| `HttpMethod`| The HTTP method to use. Defaults to `HttpMethod.GET`.| No `headers`| `dict[str, str]` or `None`| Request headers. Header values are transmitted as-is — store credentials in the plugin's [`secrets`](/sdk/secrets/) and reference them here rather than hard-coding them.| No `body`| `str` or `None`| The request body, as a string. For JSON payloads, serialize with `json.dumps(...)` and set the appropriate `Content-Type` header.| No `retry_on_status_codes`| `list[int]` or `None`| HTTP status codes that should trigger a retry. Each value must be in the range `100`–`599`. Setting this automatically routes the request through the async runner (equivalent to `.set_async(delay_seconds=0)`); use `.set_async(...)` only to override the delay or set `max_retries`.| No ## `HttpMethod` A `StrEnum` of the supported HTTP methods. You can also pass the string value as well. Member| Value ---|--- `HttpMethod.GET`| `"GET"` `HttpMethod.POST`| `"POST"` `HttpMethod.PUT`| `"PUT"` `HttpMethod.PATCH`| `"PATCH"` `HttpMethod.DELETE`| `"DELETE"` ## Security & Network Behavior The platform applies several safeguards before executing the request: - **SSRF protection.** The host is resolved and rejected if it points at a private (RFC 1918), loopback, link-local (including the `169.254.169.254` cloud metadata address), multicast, reserved, or unspecified address. Both literal IPs (e.g. `http://10.0.0.1/`) and hostnames that resolve to such addresses are blocked. - **Redirect behavior.** GET requests follow redirects normally. Non-GET methods (POST, PUT, PATCH, DELETE) are made with `allow_redirects=False`, so a `3xx` response is returned as-is rather than re-posting data to a different host. - **Request timeout.** Each request has a 30-second timeout. Requests that exceed it are aborted. - **Connection errors are swallowed.** If the upstream service is unreachable or the request fails at the transport layer, the failure is logged and the effect pipeline continues — it does not raise back into your handler. - **Redacted logging.** The platform logs the request method, host, path, and final status code. Query strings, fragments, request headers, request bodies, and response bodies are never logged, so credentials passed via query string or `Authorization` headers do not leak into platform logs. ## Async Execution Chain `.set_async(...)` onto the result of `.apply()` to have the platform schedule the request through its async runner instead of running it inline with the handler. You can read more about async effect execution [here](/sdk/effects/#async-execution). When `retry_on_status_codes` is set on the effect, the SDK automatically sets `delay_seconds=0` (async-now) so the request is routed through the async runner — you don't need to call `.set_async()` separately just for retries. The async runner uses `retry_on_status_codes` (in combination with `max_retries`) to decide whether a response should trigger a retry. ## Handling Credentials Header values are transmitted to the upstream service exactly as provided. Do not hard-code API tokens or other credentials into your plugin source. Instead, declare them as [secrets](/sdk/secrets/) in `CANVAS_MANIFEST.json` and read them at runtime via `self.secrets` on the handler. ## Example Usage ### Send a POST request asynchronously ```python import json from canvas_sdk.events import EventType from canvas_sdk.effects.http_request import HttpMethod, HttpRequestEffect from canvas_sdk.handlers import BaseHandler class MyHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.PATIENT_CREATED) def compute(self): http_effect = HttpRequestEffect( url="https://api.example.com/submit", method=HttpMethod.POST, headers={ "Authorization": f"Bearer {self.secrets['MY_API_TOKEN']}", "Content-Type": "application/json", }, body=json.dumps({"patient_id": self.target}), retry_on_status_codes=[500, 502, 503], ) # retry_on_status_codes already implies async-now; .set_async() is here to add max_retries return [http_effect.apply().set_async(max_retries=3)] ``` ### Issue a simple GET request ```python http_effect = HttpRequestEffect( url="https://api.example.com/status", ) return [http_effect.apply().set_async(delay_seconds=0)] ``` ### Schedule a delayed request ```python # Run the request 60 seconds from now http_effect = HttpRequestEffect( url="https://api.example.com/sync", method=HttpMethod.PUT, body=json.dumps({"status": "ready"}), headers={"Content-Type": "application/json"}, ) return [http_effect.apply().set_async(delay_seconds=60)] ``` ## Validation Construction is validated by Pydantic and will raise a `ValidationError` for: - An empty `url`. - A `method` that is not a member of `HttpMethod`. - A `retry_on_status_codes` entry that is not an integer or falls outside the `100`–`599` range. --- # Lab Report Effect Source: https://docs.canvasmedical.com/sdk/effect-lab-report/ The `LabReport` effect lets plugins manage a lab report's full lifecycle, independently of any lab order. It is designed for workflows where a report exists before its structured results do — for example, a scanned report that arrives by fax or upload and is OCR'd asynchronously, so the lab tests and values aren't ready until hours or days after the report is created. With these effects a plugin can: - **Create** a report up front, with no order, no PDF, and no results. - **Attach results** (lab tests and values) to that report later, as they become available. - **Update** report metadata, such as its name. - **Enter-in-error** a report so a user can self-correct a mistake. A created report is linked to the patient you supply. It starts **empty** — until you attach results it holds no tests or values, and it stays an **uncommitted draft**. A results-less draft does **not** appear in the patient's lab reports in the chart (that view only shows committed reports); it exists but isn't surfaced there yet. The first `attach_results()` commits the report — that's when it fills in, creates the observations behind its values, and appears in the chart, reading like any other lab report. It is **not** a Data Integration document, so it never appears in the Data Integration queue, and Canvas creates the report's diagnostic report and renders a document from its data automatically. ## Identifying a report Every effect references a report by one of two handles: Handle| What it is| When to use it ---|---|--- `reference_id`| A stable identifier your plugin assigns when it creates a report.| The report your plugin created. Required on `create()`. `report_id`| The [LabReport](/sdk/data-labs/)'s `id`.| Any report — including ones your plugin didn't create. Effects are fire-and-forget, so `create()` does not return the new report's `report_id`. Use the `reference_id` you assigned as your handle for the later `attach`/`update`/`enter_in_error` calls. If you need the `report_id` (for example to act on a report your plugin did not create), read it from the `LAB_REPORT_CREATED` event or query the [LabReport](/sdk/data-labs/) data model by `reference_id` (the handle you assigned is stored there; the data model's own `external_id` is reserved for electronic/Health-Gorilla feed ids). Namespace your `reference_id` values (e.g. `"my-plugin:batch-2026-06-17:img-44"`) so they don't collide with report ids from other inbound-lab sources. Build a `LabReport` and return one of its methods from your handler: `create()`, `update()`, `enter_in_error()`, or `attach_results()`. ## Methods ### create() → Effect Create a lab report decoupled from its results — no order, no PDF, and no values required. #### Validation - `reference_id` is **required** (it is your handle for attaching results later). - `patient_id` is **required**. - `report_id` must **not** be set (creation assigns the id). - The `reference_id` must not already be in use by an existing report. #### Example ```python import datetime from canvas_sdk.effects.lab_report import LabReport from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.first() report = LabReport( reference_id="my-plugin:batch-2026-06-17:img-44", patient_id=patient.id, report_name="CBC (scanned 2026-06-17)", date_performed=datetime.datetime.now(), ) effect = report.create() ``` ### update() → Effect Update report metadata, such as renaming it via `report_name`. Only the fields you set are sent. Only `report_name` and `date_performed` can be changed — `update()` **cannot move the report to a different patient** ; the patient is fixed when the report is created. If a report was attached to the wrong patient, enter it in error and recreate it on the correct patient. #### Validation - Exactly one handle (`reference_id` or `report_id`) is **required**. - At least one mutable field (`report_name` or `date_performed`) must be provided. - The report must not already be entered-in-error or reviewed by a provider. #### Example ```python from canvas_sdk.effects.lab_report import LabReport renamed = LabReport( reference_id="my-plugin:batch-2026-06-17:img-44", report_name="Complete Blood Count", ) effect = renamed.update() ``` ### enter_in_error() → Effect Flag a report as entered-in-error — use it when a report was filed incorrectly. It junks the report (removing it from active views) and records who entered it in error. The report's observations and its linked DiagnosticReport and DocumentReference records are marked entered-in-error as well. Once a report is entered-in-error (or junked) it can no longer be modified — `update()` and `attach_results()` on it raise a validation error. #### Validation - A handle (`reference_id` or `report_id`) is **required**. - Any other field is ignored (not rejected). - The report must not already be entered-in-error or reviewed by a provider. #### Example ```python from canvas_sdk.effects.lab_report import LabReport voided = LabReport(reference_id="my-plugin:batch-2026-06-17:img-44") effect = voided.enter_in_error() ``` ### attach_results() → Effect Adds lab tests and values to an existing report. See Attaching results for its arguments and an example. ## Attributes Name| Type| Description| Required ---|---|---|--- `reference_id`| `str` or `None`| The plugin-assigned handle (maximum 40 characters). **Required** when creating; usable as the handle for other operations.| For `create()`; for `update()` and `enter_in_error()`, this or `report_id` `report_id`| `UUID` or `None`| The [LabReport](/sdk/data-labs/)'s `id` (a valid uuid string is also accepted). Must be **unset** when creating; an alternative handle otherwise.| For `update()` and `enter_in_error()`, this or `reference_id` `patient_id`| `str` or `None`| The [Patient](/sdk/data-patient/)'s `id`. **Required** when creating.| For `create()` `report_name`| `str` or `None`| Human-readable report name (maps to the report's document name).| No `date_performed`| `datetime` or `None`| The report's effective/displayed date. If omitted on `create`, it defaults to the creation time — correct it later via `update`.| No ## Attaching results Once results are available, attach them with the `attach_results` method on `LabReport`. The report handle comes from the `LabReport` instance (`report_id` or `reference_id`); the method takes a list of `LabTest`s, each grouping the `LabValue`s measured for it — so the values for one test are bundled under that test in the chart. Attaching is **additive** : it appends tests and values without removing any already on the report, and Canvas creates an observation for each value automatically. Attaching results saves the report and regenerates its linked DiagnosticReport and rendered DocumentReference (the report's document) to reflect the newly attached values. The first `attach_results()` call also commits the report (a never-populated report stays an uncommitted draft). A committed report enters the lab-review workflow **review-required** and **requiring a signature** , but with **no reviewer assigned** — a clinician still has to pick it up, review, and sign it. Once a provider has reviewed it, the report is locked to further SDK edits. ### Arguments Name| Type| Description ---|---|--- `lab_tests`| list[`LabTest`]| The tests to attach. At least one is required. The report handle comes from the `LabReport` instance. ### `LabTest` A `LabTest` is a test that was performed — an ordered panel or a single analyte — and it groups its result values (a result test can carry many values). `ontology_test_code`/`ontology_test_name` are the lab's **order/compendium** code and name — _not_ LOINC. LOINC is supplied separately via `codings` (see `CodingData` below). Name| Type| Description ---|---|--- `ontology_test_code`| `str`| The lab's order/compendium code for the test. Defaults to empty string. `ontology_test_name`| `str`| Human-readable test name. Defaults to empty string. `codings`| list[`CodingData`] or `None`| The test's LOINC coding(s); only LOINC-system codings are stored. `values`| list[`LabValue`]| The result values for this test. **At least one is required.** ### `LabValue` Each `LabValue` is one measured result on its test. Name| Type| Description ---|---|--- `value`| `str`| The result value. Required. `units`| `str`| Unit of measure (e.g. `"g/dL"`). Defaults to empty string. `reference_range`| `str`| Reference range as free text. Defaults to empty string. `abnormal_flag`| `AbnormalFlag` or `None`| Flags the value against its reference range. Any non-empty flag marks the result abnormal in the lab report. Defaults to `None`. `observation_status`| `ObservationStatus`| Status of the observation. Defaults to `ObservationStatus.FINAL`. `comment`| `str`| Free-text comment. Defaults to empty string. `codings`| list[`CodingData`] or `None`| The value's LOINC coding(s); only LOINC-system codings are stored. ### `CodingData` A coding attached to a test or a value, reused from the [`Observation`](/sdk/effect-observation/) effect. Only codings whose `system` is `http://loinc.org` are persisted, and the `display` becomes the stored coding name. Name| Type| Description ---|---|--- `code`| `str`| The LOINC code (e.g. `"718-7"`). Required. `display`| `str`| Human-readable display; stored as the coding's name. `system`| `str`| Coding system URI. Use `"http://loinc.org"`. `version`| `str`| Optional coding-system version. Defaults to empty. `user_selected`| `bool`| Whether a user selected this coding. Defaults to `False`. ### `AbnormalFlag` A `StrEnum` of abnormal-result flags (HL7 v2 table 0078) for a `LabValue`. Setting any of these marks the result abnormal on the lab report. Member| Value ---|--- `HIGH`| `H` `LOW`| `L` `CRITICAL_HIGH`| `HH` `CRITICAL_LOW`| `LL` `BELOW_ABSOLUTE_LOW`| `<` `ABOVE_ABSOLUTE_HIGH`| `>` `ABNORMAL`| `A` `CRITICAL_ABNORMAL`| `AA` `SUSCEPTIBLE`| `S` `RESISTANT`| `R` `INTERMEDIATE`| `I` `NEGATIVE`| `NEG` `POSITIVE`| `POS` ### `ObservationStatus` A `StrEnum` of statuses for a `LabValue`'s observation. Defaults to `FINAL`. Member| Value ---|--- `FINAL`| `final` `PRELIMINARY`| `preliminary` `AMENDED`| `amended` `CORRECTED`| `corrected` `CANCELLED`| `cancelled` `REGISTERED`| `registered` `ENTERED_IN_ERROR`| `entered-in-error` `UNKNOWN`| `unknown` #### Validation - Exactly one of `reference_id` or `report_id` is **required**. - At least one `LabTest` is **required** , and each `LabTest` requires at least one `LabValue`. - The report must exist and must not be entered-in-error or reviewed by a provider. #### Example A SimpleAPI route an OCR service calls once it has abstracted the values: ```python from canvas_sdk.effects.lab_report import LabReport, LabTest, LabValue from canvas_sdk.effects.observation import CodingData from canvas_sdk.effects.simple_api import JSONResponse, Response from canvas_sdk.handlers.simple_api import APIKeyAuthMixin, SimpleAPIRoute LOINC = "http://loinc.org" class LabResultsAPI(APIKeyAuthMixin, SimpleAPIRoute): PATH = "/lab-results" def post(self) -> list[Response]: body = self.request.json() return [ LabReport(reference_id=body["reference_id"]).attach_results( [ LabTest( ontology_test_code=test.get("order_code", ""), ontology_test_name=test.get("name", ""), codings=( [CodingData(code=test["loinc"], display=test.get("name", ""), system=LOINC)] if test.get("loinc") else None ), values=[ LabValue( value=value["value"], units=value.get("units", ""), reference_range=value.get("reference_range", ""), codings=( [CodingData(code=value["loinc"], display=value.get("name", ""), system=LOINC)] if value.get("loinc") else None ), ) for value in test["values"] ], ) for test in body["tests"] ] ), JSONResponse({"reference_id": body["reference_id"]}, status_code=202), ] ``` ## Example Workflow The four effects compose into the asynchronous OCR workflow: 1. A scanned report arrives. The plugin calls `LabReport(reference_id=..., patient_id=..., ...).create()`, keying off an `reference_id` it controls. 2. Days later, the OCR service finishes. The plugin calls `LabReport(reference_id=...).attach_results([LabTest(..., values=[LabValue(...)])])` to attach the abstracted tests and values — the report's observations populate from there. 3. To fix the report name, the plugin calls `LabReport(reference_id=..., report_name=...).update()`. 4. If the report was filed in error, the plugin calls `LabReport(reference_id=...).enter_in_error()`. ## Related - [`Observation`](/sdk/effect-observation/) — create or update individual clinical observations. --- # Message Effect Source: https://docs.canvasmedical.com/sdk/effect-messages/ The `Message` effect provides a unified way to create, edit, and transmit messages between users (patients or staff) within the Canvas platform. It supports standalone creation, immediate send after creating, edits, and dedicated send operations. Build a `Message` with the attributes you want to set, then return one of its methods from your handler: `create()`, `create_and_send()`, `edit()`, or `send()`. To route a message to a group of staff rather than one person, assign it to a team. See Assigning a message to a team. ## Methods ### create() → Effect Creates a message without sending it. - `content`, `sender_id`, and `recipient_id` are required, and `content` cannot be empty. - `message_id` must not be set. - `team_id` is optional. Set it to create the message already assigned to a team. ### create_and_send() → Effect Creates the message and sends it in one operation. - `content`, `sender_id`, and `recipient_id` are required, and `content` cannot be empty. - `message_id` must not be set. - `team_id` is optional. Set it to create the message already assigned to a team. - When the recipient is a patient, the patient needs an eligible contact point. See Sending to a patient. ### edit() → Effect Changes an existing message. - `message_id` is required, and must be the id of an existing message. - Only the attributes you set on the effect are changed. Attributes you leave unset keep their current values. - `attachment_upload_keys` adds files to the message, and can be the only attribute you set. A locked message can't take new files. See Attaching files. - `team_id` must not be set. Editing a message keeps its team. To change the team, use `AssignMessageTeam`. ### send() → Effect Sends a message that was already created, if you separated creating it from sending it. - Set `message_id` to the message to send. - `attachment_upload_keys` must not be set. Attach files with `create()`, `create_and_send()`, or `edit()` before sending. - `team_id` must not be set. Use `AssignMessageTeam` to change the team. - When the recipient is a patient, the patient needs an eligible contact point. See Sending to a patient. ## Attaching files To attach files to a message, set `attachment_upload_keys` to a list of upload keys. Each key is the `key` of a `StoredFilePart` that a user uploaded to your plugin through a SimpleAPI route declared with `file_uploads="stored"`. See [Storing uploaded files](/sdk/handlers-simple-api-http/#storing-uploaded-files). Attachments can be PNG, JPEG, or PDF files, and each key must come from a file your plugin uploaded. When the effect is applied, Canvas adds the files in the same transaction as the message, before the message is sent. Canvas then deletes the uploads, so each key works only once. This SimpleAPI route sends the files in a `multipart/form-data` request to a patient, from the logged-in staff member: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.note.message import Message from canvas_sdk.effects.simple_api import JSONResponse, Response from canvas_sdk.handlers.simple_api import SimpleAPI, StaffSessionAuthMixin, api class SendFilesAPI(StaffSessionAuthMixin, SimpleAPI): @api.post("/send-files", file_uploads="stored") def send_files(self) -> list[Response | Effect]: form = self.request.form_data() message = Message( content="Here is the form we discussed.", sender_id=self.request.headers["canvas-logged-in-user-id"], recipient_id=form["patient_id"].value, attachment_upload_keys=[part.key for part in form.get_list("files") if not part.error], ) return [message.create_and_send(), JSONResponse({})] ``` To read a message's attachments, see [MessageAttachment](/sdk/data-message/#messageattachment). ## Sending to a patient When `create_and_send()` or `send()` sends a message to a patient, Canvas notifies the patient the same way it does for a message sent from the chart: a text message or email to each of the patient's phone numbers and email addresses that is eligible. A contact point is eligible when all three of these are true: - `has_consent` is `True` - `last_verified` is set - `state` is `"active"` If the patient has no eligible contact point, Canvas cannot deliver the message. The message is saved but not sent, it appears as a draft in the patient's chart, and the effect fails. To avoid this, check the patient's [contact points](/sdk/data-patient/#patientcontactpoint) before sending, and use `create()` instead when none is eligible: ```python has_eligible_contact = patient.telecom.filter( has_consent=True, last_verified__isnull=False, state="active", ).exists() message = Message(content=content, sender_id=staff.id, recipient_id=patient.id) effect = message.create_and_send() if has_eligible_contact else message.create() ``` Instances set up to deliver patient messages through a plugin, rather than through Canvas, do not send these notifications. `create()` on its own never notifies the patient. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `message_id`| `str` or `UUID` or `None`| Unique identifier of an existing message. Must be unset when creating a new message.| For `edit()` and `send()` `content`| `str` or `None`| The text body of the message. Cannot be empty.| For `create()` and `create_and_send()` `sender_id`| `str` or `UUID`| ID of the user (Patient or Staff) who is sending the message.| Yes `recipient_id`| `str` or `UUID`| ID of the user (Patient or Staff) who will receive the message.| Yes `read`| `datetime` or `None`| Timestamp indicating when the message was read by the recipient. Defaults to `None` (unread).| No `attachment_upload_keys`| `list[str]` or `None`| Upload keys of PNG, JPEG, or PDF files to attach. Not allowed with `send()`. See Attaching files.| No `team_id`| `str` or `UUID` or `None`| ID of the [team](/sdk/data-team/) to assign the new message to. Not allowed with `edit()` or `send()`. See Assigning a message to a team.| No ## Validation & Errors Before any effect is emitted, the model runs these checks: - **Sender and Recipient Exist** Verifies that both `sender_id` and `recipient_id` belong to either a `Patient` or a `Staff` record. - **Create vs. Edit Constraints** - **Create** and **Create-and-Send** must **not** include `message_id`. - **Create** and **Create-and-Send** must include non-empty `content` (content cannot be blank or whitespace-only). - **Edit** operations **must** include a valid `message_id` that already exists in the database. - **Team Exists** When `create()` or `create_and_send()` sets `team_id`, it must be the id of an existing team. `edit()` and `send()` reject `team_id`. ## Caveats - **Role Constraints:** Sender and Recipient must always be one of Patient or Staff. Patient-to-Patient messaging is not allowed. - **UI Refresh Required:** Due to system constraints, editing a message requires a manual UI refresh for updated content to display. - **Immediate Post for Patient-to-Staff:** Messages created from a Patient to Staff cannot be drafted and will immediately appear in the timeline. This means that `CREATE_AND_SEND` and `SEND` effects will fail in these scenarios. You should only use the `CREATE` method for Patient-to-Staff messaging. ## Example Usage ```python from canvas_sdk.v1.data.message import Message as MessageModel from canvas_sdk.v1.data.patient import Patient from canvas_sdk.v1.data.staff import Staff from canvas_sdk.effects.note.message import Message staff = Staff.objects.first() patient = Patient.objects.first() ``` ### Create (originate) only ```python m1 = Message( content="Your lab results are available.", sender_id=staff.id, recipient_id=patient.id ) effect_create = m1.create() ``` ### Create and send in one step ```python m2 = Message( content="Your appointment is confirmed.", sender_id=staff.id, recipient_id=patient.id ) effect_create_and_send = m2.create_and_send() m = MessageModel.objects.get(message_id="msg-1234") ``` ### Edit an existing message ```python m3 = Message( message_id=m.id, content="Updated: Your appointment has moved to 3pm." ) effect_edit = m3.edit() ``` ### Send an existing message ```python m4 = Message(message_id=m.id) effect_send = m4.send() ``` ## Assigning a message to a team A message always has one staff or patient `recipient`. It can also be assigned to a [team](/sdk/data-team/), so a group such as a front desk or a care team can work messages together. Assigning a team does not change the `recipient`. In the global Messages panel, the assignee filter defaults to **Me or my teams** , so staff see messages addressed to them and messages assigned to any of their teams, and the unread badge counts both. A message has a single read timestamp, so when one team member reads a team message, it is read for the whole team. There are two ways to assign a team: - Set `team_id` when you create the message with `create()` or `create_and_send()`. - Return an `AssignMessageTeam` effect to set, change, or clear the team of an existing message. ### AssignMessageTeam Build an `AssignMessageTeam` and return its `apply()` from your handler. #### Methods ##### apply() → Effect Sets, changes, or clears the team of an existing message. - `message_id` is required, and must be the id of an existing message. - `team_id` is required. Set it to `None` to unassign the message. - The effect fails validation, and the message keeps its current team, when either id is not a valid UUID or does not match an existing message or team. - Each assignment, reassignment, and unassignment emits the [`MESSAGE_TEAM_ASSIGNED`](/sdk/events/#messaging) event. Creating a message with a team emits it too. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `message_id`| `str` or `UUID`| ID of the [message](/sdk/data-message/#message) to assign.| Yes `team_id`| `str` or `UUID` or `None`| ID of the [team](/sdk/data-team/) to assign the message to, or `None` to unassign it.| Yes #### Example This handler assigns each message a patient sends to the team named "Front Desk": ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.note.message import AssignMessageTeam from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler from canvas_sdk.v1.data import Message, Team class RoutePatientMessages(BaseHandler): """Assign messages that patients send to the Front Desk team.""" RESPONDS_TO = EventType.Name(EventType.MESSAGE_CREATED) def compute(self) -> list[Effect]: message = Message.objects.select_related("sender").filter(id=self.target).first() # Only route messages a patient sent, not messages staff sent to a patient. if message is None or message.sender is None or message.sender.is_staff: return [] team = Team.objects.filter(name="Front Desk").first() if team is None: return [] return [AssignMessageTeam(message_id=message.id, team_id=team.id).apply()] ``` --- # Note Custom Content Effect Source: https://docs.canvasmedical.com/sdk/effect-note-custom-content/ The `NoteCustomContent` effect adds a custom content block to a clinical note. Use it to show plugin-generated information where clinicians already work in the note. For example, show a patient summary above the note body or a card inside a note section. A block can be an inline HTML string or the URL of a page your plugin serves. Return this effect in response to the [`NOTE__GET_CUSTOM_CONTENT`](/sdk/events/#note-custom-content) event. If no handler returns one, the note renders without custom content. > **Warning:** **Beta Participation:** Custom content is part of the [revamped note beta program](/product-updates/note-revamp/), which Canvas turns on per instance. On an instance outside the beta, Canvas doesn't fire `NOTE__GET_CUSTOM_CONTENT`, and notes render without custom content. Reach out to Canvas support if you would like to participate. * * * ## How it works When a note renders, Canvas fires `NOTE__GET_CUSTOM_CONTENT` targeting that note. `event.target.id` holds the note's id. A handler subscribed to the event returns one or more `NoteCustomContent` effects, and Canvas renders each block in the section it names. - **Placement:** Set `section` to place a block at the top of that note section. Leave `section` unset to place the block above the note body. - **Note views:** Blocks with a `section` appear only while the note is grouped into sections, and are hidden while their section is collapsed. When a user views the whole note instead, or sections are turned off for the instance, only blocks without a `section` appear. - **Multiple blocks:** A handler can return several effects. Blocks returned for the same section stack in the order the handlers ran. - **Frames:** Canvas renders each block in its own iframe, so one plugin's block can't reach another's. - **Sections:** `section` names a section as the note renders it, not the clinical note section of a command. Canvas decides which commands each rendered section gathers, so a plugin that targets a rendered section keeps working if that grouping changes. ### Attributes Field| Type| Default| Description ---|---|---|--- `section`| `NoteCustomContent.Section` or `None`| `None`| The note section to render the block in. When unset, the block renders above the note body. `url`| `str`| `None`| The URL of a page to render in the block, such as a [SimpleAPI](/sdk/handlers-simple-api/) route your plugin serves. `content`| `str`| `None`| An HTML string to render in the block. Set exactly one of `url` or `content`. The effect raises a validation error when both are set or when neither is set. ### Sections `section` accepts a member of the `NoteCustomContent.Section` enum. Plain strings such as `"exam"` fail validation. Member| Section ---|--- `NoteCustomContent.Section.HISTORY`| History `NoteCustomContent.Section.EXAM`| Exam `NoteCustomContent.Section.ASSESSMENT_PLAN`| Assessment & Plan `NoteCustomContent.Section.INTERNAL`| Internal ### Choose between `content` and `url` Use `content` for a static block you can build as an HTML string inside the handler. Use `url` for a block that updates itself after the note renders, as described in Keeping content current. Canvas renders inline `content` in a frame it manages, and a [WebSocket](/sdk/handlers-simple-api-websocket/) opened from that frame receives no messages. A page served from your plugin keeps its WebSocket connection open and can receive `Broadcast` messages. ### Keeping content current Canvas fires `NOTE__GET_CUSTOM_CONTENT` when a note renders, and doesn't fire it again as the note changes. To keep a block current, hold its state in the page and push updates to it with a `Broadcast` effect, rather than expecting Canvas to ask for the content again: 1. Serve the block from a `url`, and have the page connect to a [WebSocket](/sdk/handlers-simple-api-websocket/) channel your plugin defines. Use one channel per note, such as `note-`, so a block hears only about the note it's rendered in. 2. From a handler that responds to the events that change the block, return a [`Broadcast`](/sdk/handlers-simple-api-websocket/#broadcasting-messages) effect for that channel. Put the new data in the message, so the page can update without fetching anything. For example, a block that counts the note's commands can broadcast new counts when a command is added, deleted, or entered in error. Each command type fires its own events for these, such as `PLAN_COMMAND__POST_ORIGINATE`, `PLAN_COMMAND__POST_DELETE`, and `PLAN_COMMAND__POST_ENTER_IN_ERROR`. This handler broadcasts to a note's channel when a Plan or Vitals command is added to the note: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.simple_api import Broadcast from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler from canvas_sdk.v1.data.command import Command class NotifyNoteBlocks(BaseHandler): RESPONDS_TO = [ EventType.Name(EventType.PLAN_COMMAND__POST_ORIGINATE), EventType.Name(EventType.VITALS_COMMAND__POST_ORIGINATE), ] def compute(self) -> list[Effect]: note_id = ( Command.objects.filter(id=self.event.target.id) .values_list("note__id", flat=True) .first() ) if not note_id: return [] return [ Broadcast( channel=f"note-{note_id}", message={ "event_type": "command_added", "command_id": self.event.target.id, }, ).apply() ] ``` ### Permissions for URL blocks A `url` block's frame receives the permissions your plugin registers for that URL in the [`url_permissions`](/sdk/canvas_manifest/#url-permissions) section of `CANVAS_MANIFEST.json`, such as `MICROPHONE` or `ALLOW_SAME_ORIGIN`. The format is the same as for [modals and widgets](/sdk/layout-effect/#additional-configuration). Inline `content` blocks receive no permissions. ### Frame height Each frame starts 120 pixels tall. To fit the frame to your block, send a `RESIZE` message with the block's height over the message channel Canvas opens with the frame: ```html ``` ### Examples Return an inline block above the note body: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.note_custom_content import NoteCustomContent from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler class NoteReminder(BaseHandler): RESPONDS_TO = EventType.Name(EventType.NOTE__GET_CUSTOM_CONTENT) def compute(self) -> list[Effect]: return [ NoteCustomContent( content="

Review the patient's care plan before signing.

", ).apply() ] ``` Return a block for each section, each pointing at a page the plugin serves. Here `/plugin-io/api/my_plugin/section` stands in for a SimpleAPI route in your plugin that returns the block's HTML: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.note_custom_content import NoteCustomContent from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler BASE_URL = "/plugin-io/api/my_plugin" class SectionCards(BaseHandler): RESPONDS_TO = EventType.Name(EventType.NOTE__GET_CUSTOM_CONTENT) def compute(self) -> list[Effect]: note_id = self.event.target.id return [ NoteCustomContent( section=section, url=f"{BASE_URL}/section?note_id={note_id}§ion={section.value}", ).apply() for section in NoteCustomContent.Section ] ``` For a complete plugin that serves its blocks as pages and keeps them current over a WebSocket, see the [`note_custom_content` example plugin](https://github.com/canvas-medical/canvas-plugins/tree/main/example-plugins/note_custom_content). --- # Note Footer Configuration Effect Source: https://docs.canvasmedical.com/sdk/effect-note-footer-configuration/ The `NoteFooterConfiguration` effect configures the note footer at the note level (rather than per button). Its primary use is hiding Canvas's default state-transition buttons — Lock, Sign, Push charges, Delete, and so on — so that a plugin can supply its own footer buttons in their place, such as with [Note State Action Buttons](/sdk/handlers-action-buttons/#note-state-action-buttons). Return this effect in response to the `NOTE_FOOTER__GET_CONFIGURATION` event, which fires when a note's footer is loaded. If your handler does not return a configuration, the default state-transition buttons remain visible. Build a `NoteFooterConfiguration` with the attributes you want to set, then return its `apply()` from your handler. * * * ## Methods ### apply() → Effect Configures the footer of the note the event targets. No attribute is required. ## How it works As a note's footer loads, Canvas fires `NOTE_FOOTER__GET_CONFIGURATION` targeting that note's external id. A handler subscribed to the event returns a `NoteFooterConfiguration` effect to configure the footer. If no plugin returns one, the footer keeps its default configuration. ### Event payload Property| Value| Description ---|---|--- `event.target.id`| `str` (UUID)| The external id of the [Note](/sdk/data-note/#note) whose footer is loading. `event.actor`| user| The logged-in user viewing the note, when available. `event.context`| `{}`| Empty — no additional context is provided. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `hide_default_state_buttons`| `bool`| Hide Canvas's native footer state-transition buttons for this note. Defaults to `False`.| No ## Example ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.note_footer_configuration import NoteFooterConfiguration from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler class HideDefaultStateButtons(BaseHandler): RESPONDS_TO = EventType.Name(EventType.NOTE_FOOTER__GET_CONFIGURATION) def compute(self) -> list[Effect]: return [NoteFooterConfiguration(hide_default_state_buttons=True).apply()] ``` --- # NoteMetadata Effect Source: https://docs.canvasmedical.com/sdk/effect-note-metadata/ The `Note.upsert_metadata` method provides a flexible key-value storage system for note-specific data within the Canvas system. This method enables the creation and updating of custom metadata entries associated with note records, allowing for extensible note information storage beyond standard note fields. ## Overview Note metadata serves as a powerful extension mechanism for storing custom note-related information that doesn't fit within the standard note data model. Metadata is managed through the `upsert_metadata` method on the `Note` effect class. ## Methods ### upsert_metadata(key: str, value: str) → Effect Creates or updates a metadata entry for the specified note. #### Parameters Parameter| Type| Description| Required ---|---|---|--- `key`| `str`| Unique identifier for the metadata entry within the note context| Yes `value`| `str`| The metadata value to store| Yes #### Prerequisites The `Note` effect must be initialized with an `instance_id` corresponding to an existing note. Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Id of the note record to associate metadata with| Yes #### Returns An `Effect` object configured for upserting note metadata. #### Behavior - If a metadata entry with the specified key already exists for the note, it will be updated with the new value - If no entry exists, a new metadata entry will be created - The operation is idempotent - repeated calls with the same key and value will not create duplicate entries - Raises `ValueError` if `instance_id` is not set on the `Note` effect ## Implementation Details ### Validation The effect performs comprehensive validation before execution: 1. **Note Existence Validation** : Verifies that the referenced note exists in the system - Queries the note database to confirm the `instance_id` corresponds to an existing note record - Returns a descriptive error if the note is not found 1. **Field Validation** : Ensures all required fields are provided and properly formatted - `instance_id` must be set on the `Note` effect - Both `key` and `value` must be provided ## Example Usage ### Basic Usage ```python from canvas_sdk.effects.note.note import Note # Create a metadata entry for note tracking note = Note(instance_id="803ce56a-350e-49a4-abae-019d9f5f24b2") effect = note.upsert_metadata(key="my_plugin:external_system_id", value="EXT-12345") ``` ### Example ```python import json from canvas_sdk.effects import Effect from canvas_sdk.effects.note.note import Note from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class NoteMetadataHandler(BaseHandler): """ Adds metadata to notes when a plan command is updated. """ RESPONDS_TO = EventType.Name(EventType.PLAN_COMMAND__POST_UPDATE) def compute(self) -> list[Effect]: note_id = self.event.context["note"]["id"] command_id = self.event.target.id note = Note(instance_id=note_id) return [note.upsert_metadata(key="my_plugin:last_plan_update_command", value=str(command_id))] ``` ### Storing Multiple Metadata Entries ```python import json from canvas_sdk.effects import Effect from canvas_sdk.effects.note.note import Note from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class SigningMetadataHandler(BaseHandler): """ Adds metadata to notes when they are signed. """ RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED) def compute(self) -> list[Effect]: note_id = self.event.context["note"]["id"] state = self.event.context.get("note_state_change_event", {}).get("state") effects: list[Effect] = [] if state == "SGN": note = Note(instance_id=note_id) # Store signing source effects.append(note.upsert_metadata(key="my_plugin:signing_source", value="protocol")) # Store additional context as JSON context_data = { "signed_by": self.event.context.get("actor", {}).get("id"), "protocol_version": "1.0" } effects.append(note.upsert_metadata(key="my_plugin:signing_context", value=json.dumps(context_data))) return effects ``` ## Best Practices ### Key Naming Conventions 1. **Use Descriptive Names** : Choose keys that clearly indicate the purpose of the metadata - Good: `external_system_id`, `workflow_stage`, `signing_source` - Avoid: `data1`, `temp`, `misc` 1. **Namespace Your Keys** : Prefix keys with your plugin name to avoid collisions with other plugins - Example: `my_plugin:external_system_id`, `my_plugin:workflow_stage`, `my_plugin:signing_source` ### Value Storage 1. **String Serialization** : All values are stored as strings. For complex data types: ```python import json from canvas_sdk.effects.note.note import Note note = Note(instance_id="803ce56a-350e-49a4-abae-019d9f5f24b2") complex_data = {"stage": "review", "approvers": ["user1", "user2"], "timestamp": "2025-01-15T10:30:00Z"} note.upsert_metadata(key="my_plugin:workflow_state", value=json.dumps(complex_data)) ``` 2. **Boolean Values** : Store as "true" or "false" strings for consistency ```python from canvas_sdk.effects.note.note import Note needs_followup = True note = Note(instance_id="803ce56a-350e-49a4-abae-019d9f5f24b2") note.upsert_metadata(key="my_plugin:requires_followup", value="true" if needs_followup else "false") ``` ## Notes - Metadata entries are note-specific and isolated - the same key can have different values for different notes - There is no built-in versioning; updating a key overwrites the previous value - The system does not enforce any schema on metadata values - validation is the responsibility of the implementing code --- # Note Restrictions Effect Source: https://docs.canvasmedical.com/sdk/effect-note-restrictions/ The `NoteRestrictionsEffect` and `NoteRestrictionsUpdatedEffect` allow plugins to restrict access to notes and push real-time permission updates to connected clients. - **`NoteRestrictionsEffect`** — returned by a plugin in response to a `GET_NOTE_RESTRICTIONS` event. It tells the Canvas UI whether the requesting user should see a banner, have the note content blurred, or have editing disabled. - **`NoteRestrictionsUpdatedEffect`** — emitted by a plugin at any time to signal that restrictions for a specific note have changed, causing all connected clients viewing that note to immediately refetch their restrictions. * * * ## NoteRestrictionsEffect To restrict a note for the requesting user, build a `NoteRestrictionsEffect` and return its `apply()` from your handler. ### Methods #### apply() → Effect Applies the restrictions to the user who opened the note. No attribute is required; an effect with none set leaves the note unrestricted. ### How it works Every time a note is opened (or its restrictions are refetched), Canvas fires a `GET_NOTE_RESTRICTIONS` event targeting that note's external ID. Plugins that subscribe to this event can return a `NoteRestrictionsEffect` to control what the user sees. If no plugin returns a `NoteRestrictionsEffect`, the note is unrestricted by default. ### Event payload Property| Value| Description ---|---|--- `event.target.id`| `str` (UUID)| The `id` of the note being accessed. Use this to look up the note or its metadata. `event.actor.id`| `str` (int)| The database ID of the authenticated user requesting the note. Use `Staff.objects.filter(user__dbid=event.actor.id)` to resolve to a staff record. `event.context`| `{}`| Empty — no additional context is provided. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `restrict_access`| `bool`| Whether the requesting user is restricted from editing this note. Defaults to `False`.| No `blur_content`| `bool`| Whether the note body should be blurred for the requesting user. Defaults to `False`.| No `banner_message`| `str` | `None`| Message shown in the warning banner at the top of the note. If `None`, a default "This note is currently restricted." message is displayed. Defaults to `None`.| No ### Example ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.note.restrictions import NoteRestrictionsEffect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class NoteAccessHandler(BaseHandler): """Restrict access to notes based on custom business logic.""" RESPONDS_TO = EventType.Name(EventType.GET_NOTE_RESTRICTIONS) def compute(self) -> list[Effect]: note_id = self.event.target.id actor_id = self.event.actor.id if not self.user_can_access(note_id, actor_id): return [ NoteRestrictionsEffect( restrict_access=True, blur_content=True, banner_message="You do not have permission to view this note.", ).apply() ] return [] def user_can_access(self, note_id: str, actor_id: str) -> bool: # Custom access logic here ... ``` * * * ## NoteRestrictionsUpdatedEffect To push a restriction change to everyone viewing a note, build a `NoteRestrictionsUpdatedEffect` and return its `apply()` from your handler. ### Methods #### apply() → Effect Tells every client viewing the note to refetch its restrictions. - `note_id` is required. ### How it works When a plugin performs an action that changes whether a note is restricted (e.g. writing an edit lock to `NoteMetadata`, updating an access rule), it can emit a `NoteRestrictionsUpdatedEffect`. Canvas will broadcast a WebSocket notification to all clients currently viewing that note, causing them to refetch their restrictions immediately — no page reload required. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `note_id`| `str` (UUID)| The id of the note whose restrictions have changed.| Yes ### Example ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.note.restrictions import NoteRestrictionsUpdatedEffect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data import NoteMetadata class NoteAccessChangedHandler(BaseHandler): """Broadcast a real-time restriction update after note metadata changes.""" RESPONDS_TO = EventType.Name(EventType.NOTE_METADATA_UPDATED) def compute(self) -> list[Effect]: note_id = ( NoteMetadata.objects .filter(id=self.event.target.id) .values_list("note__id", flat=True) .first() ) if not note_id: return [] return [NoteRestrictionsUpdatedEffect(note_id=str(note_id)).apply()] ``` * * * ## Common use cases - **Concurrent edit protection** — prevent multiple providers from editing the same note simultaneously; the second user sees a banner and disabled inputs until the first provider's session expires. - **Role-based note type access** — restrict certain note types (e.g. sensitive clinical notes) to a specific set of staff members. - **Sensitive note hiding** — blur the content of notes containing sensitive information for users who should not see the full details. For full working implementations of these patterns, see the [**note-timeline-restrictions**](https://github.com/canvas-medical/canvas-plugins/tree/main/example-plugins/note-timeline-restrictions) example plugin, which covers concurrent edit locking, role-based access via a dashboard, automatic lock expiry via a cron job, and real-time updates. --- # Note Effects Source: https://docs.canvasmedical.com/sdk/effect-notes/ The Canvas SDK provides effects to facilitate creating, updating, and managing **visit notes** , **appointments** , and **schedule events**. Below you'll find detailed documentation for each effect type and their operations. ## Note Effect The `Note` effect facilitates the creation and updating of visit notes for patients. Build a `Note` with the attributes the operation needs, then return one of its methods from your handler. Each section below covers one method and lists its attributes: `create()`, `update()`, `push_charges()`, `lock()`, `sign()`, `unlock()`, `check_in()`, `no_show()`, `delete()`, `undelete()`, `discharge()`, and `upsert_metadata()`. Faxing a note uses a separate `FaxNoteEffect`, covered under Fax Note. ### Create Note `create() → Effect` Creates a new note. Can be passed an optional UUID as `instance_id` from the `uuid.uuid4` library, or will be assigned one if not present. Passing a user-set UUID as the `instance_id` allows for [assigning commands to the note](/sdk/commands/#chaining-methods-with-a-user-set-uuid) in the same plugin action. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier for the note| No `note_type_id`| `UUID` or `str`| Identifier for the note type| Yes `datetime_of_service`| `datetime.datetime`| When the service was provided| Yes `patient_id`| `str`| Identifier for the patient| Yes `practice_location_id`| `UUID` or `str`| Identifier for the practice location| Yes `provider_id`| `str`| Identifier for the provider| Yes `title`| `str` or `None`| Optional title for the note| No `supervising_provider_id`| `str` or `None`| Staff identifier for the supervising provider| No #### Implementation Details - Validates that the note type exists and has an appropriate category - Ensures the patient exists in the system - Verifies that the practice location and provider are valid - If `supervising_provider_id` is provided, validates that the Staff record exists > **Info:** You can set a custom `title` on any non–Schedule Event note type, regardless of the note type's `allow_custom_title` value. The `allow_custom_title` field on `NoteType` only applies to Schedule Event (Event Type) note types — it controls whether a custom description can be set on those events (stored on `note.description`) and is only enforced by the ScheduleEvent effect. It is not checked when the `Note` effect creates a chart note, so there is no need to gate the `title` on `allow_custom_title`. #### Example Usage ```python import datetime from canvas_sdk.effects.note.note import Note from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): note_effect = Note( note_type_id="note-type-uuid", datetime_of_service=datetime.datetime.now(), patient_id="patient-uuid", practice_location_id="practice-location-uuid", provider_id="provider-uuid" ) return [note_effect.create()] ``` To create a note and add commands to it in the same `compute()`, give the note an `instance_id` you generate, use that same value as each command's `note_uuid`, and return the note's `create()` effect before the command effects. Canvas applies a handler's effects in the order they are returned, so the note exists by the time the commands are originated: ```python import datetime import uuid from canvas_sdk.commands import PlanCommand from canvas_sdk.effects.note.note import Note from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): note_id = str(uuid.uuid4()) note_effect = Note( instance_id=note_id, note_type_id="note-type-uuid", datetime_of_service=datetime.datetime.now(), patient_id="patient-uuid", practice_location_id="practice-location-uuid", provider_id="provider-uuid", ) plan = PlanCommand(note_uuid=note_id, narrative="Follow up in two weeks") return [note_effect.create(), plan.originate()] ``` ### Update Note `update() → Effect` Updates an existing note. Only certain fields can be modified after creation. #### Attributes Attribute| Type| Description| Required| Updatable ---|---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of the note to update| Yes| No `title`| `str` or `None`| Updated title for the note| No| Yes `datetime_of_service`| `datetime.datetime`| Updated service date/time| No| Yes `practice_location_id`| `UUID` or `str`| Updated practice location| No| Yes `provider_id`| `str`| Updated provider| No| Yes `supervising_provider_id`| `str` or `None`| Staff identifier for the supervising provider| No| Yes **Note** : `patient_id` and `note_type_id` cannot be updated after creation. #### Example Usage ```python import datetime from canvas_sdk.effects.note.note import Note from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): note_effect = Note(instance_id="existing-note-uuid") note_effect.title = "Updated Consultation Notes" note_effect.datetime_of_service = datetime.datetime.now() return [note_effect.update()] ``` ### Fax Note `FaxNoteEffect.apply() → Effect` Sends an existing note via fax to a specified recipient. This effect allows you to transmit patient notes to external healthcare providers or facilities. To fax a referral, imaging order, lab order or letter without the rest of its note, use the [fax effects](/sdk/effect-fax/). #### Attributes Attribute| Type| Description| Required ---|---|---|--- `note_id`| `UUID` or `str`| Identifier of the note to fax| Yes `recipient_name`| `str`| Name of the fax recipient| Yes `recipient_fax_number`| `str`| Fax number of the recipient. Should include the country code| Yes `include_coversheet`| `bool`| Whether to include a coversheet with the fax| No `subject`| `str` or `None`| Subject line for the coversheet (required if coversheet used)| No `comment`| `str` or `None`| Additional comments for coversheet (required if coversheet used)| No `location_id`| `UUID` or `str` or `None`| Practice location ID (required if coversheet used)| No #### Implementation Details - Validates that the note exists in the system - If `include_coversheet` is `True`, the following fields become required: - `subject`: The subject line for the coversheet - `comment`: Additional comments to include on the coversheet - `location_id`: The practice location identifier (must exist in the system) - Validates that the practice location exists if provided - The fax is recorded as a [NoteActionEvent](/sdk/data-fax/#noteactionevent) on the note, so a plugin reads whether it was delivered through `note.action_events`. See [Faxes sent by a plugin](/sdk/data-fax/#faxes-sent-by-a-plugin) #### Example Usage ```python from canvas_sdk.effects.fax.note import FaxNoteEffect from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): # Basic fax without coversheet fax_effect = FaxNoteEffect( note_id="existing-note-uuid", recipient_name="Dr. Jane Smith", recipient_fax_number="15551234567" ) return [fax_effect.apply()] ``` #### Example with Coversheet ```python from canvas_sdk.effects.fax.note import FaxNoteEffect from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): # Fax with coversheet fax_effect = FaxNoteEffect( note_id="existing-note-uuid", recipient_name="Dr. Jane Smith", recipient_fax_number="15551234567", include_coversheet=True, subject="Patient Referral - Follow-up Care", comment="Please review attached consultation notes for continuing care.", location_id="practice-location-uuid" ) return [fax_effect.apply()] ``` ### Note state transitions The state change effects below (push charges, lock, sign, unlock, check in, no show, delete, undelete, and discharge) each move a note to a new [state](/sdk/data-note/#notestates), and each is accepted only from certain current states. Which transitions a note allows depends on its note type's category, and on whether the type is billable or requires a signature. An effect applied from any other state raises `ValueError: Invalid state transition`. Effect| Moves the note to| Allowed from ---|---|--- `push_charges()`| `PSH`| `NEW`, `CVD`, `PSH`, `ULK`, `UND`. Billable encounter note types only, and only when the Push charges button is enabled. `lock()`| `LKD`| `NEW`, `CVD`, `ULK`, `UND`, and also `PSH` for encounter notes. Appointment notes lock only from `NSW`. `sign()`| `SGN`| `LKD`, or `SGN` to sign again. Note types with `is_sig_required` only. `unlock()`| `ULK`| `LKD`, and also `SGN` for note types with `is_sig_required`. On appointment notes, unlocking moves `LKD` back to `NSW`. `check_in()`| `CVD`| `BKD`, `RVT`, `NSW`. Appointment notes only. `no_show()`| `NSW`| `BKD`, `RVT`. Appointment notes only. `delete()`| `DLT`| `NEW`, `CVD`, `ULK`, `UND`, and also `PSH` for encounter notes. Appointment notes delete only from `SCH`. `undelete()`| `UND`| `DLT`. Not available for appointment notes. `discharge()`| `DSC`| `NEW`, `CVD`, `ULK`, `UND`. Inpatient notes only. Notes in the remaining categories, such as message, letter, data, and search notes, accept none of these effects. ### Push Charges `push_charges() → Effect` Pushes the charges from the Note to its associated Claim in the Revenue module. Has the exact same effect as clicking on the `Push charges` button in the Note footer. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of the note to update| Yes **Note** : `instance_id` must be a valid, existing Note, and its NoteTypeVersion must have `is_billable` = True. #### Example Usage ```python import datetime from canvas_sdk.effects.note.note import Note from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): note_effect = Note(instance_id="existing-note-uuid") return [note_effect.push_charges()] ``` > **Info:** This effect will be originated by the current actor that triggered the event, with a fallback to Canvas Bot if no actor is found. To change the charge amount on a claim that already exists, use the [`UpdateClaimLineItem`](/sdk/effect-claims/#updateclaimlineitem) effect. Alternatively, add [BillingLineItems](/sdk/effect-billing-line-items/) to the patient's note and then push charges so the claim is updated appropriately. The FHIR Claim API only supports changing the queue or adding a comment, so charge amounts cannot be edited there. To move a claim into a specific revenue queue from a plugin — for example, to route contract-based claims that do not go through a clearinghouse into a queue your team works manually — use [`ClaimEffect.move_to_queue()`](/sdk/effect-claims/#move-to-queue). ### Lock `lock() → Effect` Locks an existing note, preventing further modifications. Has the exact same effect as clicking on the `Lock` button in the Note footer. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of the note to lock| Yes **Note** : `instance_id` must be a valid, existing Note that is not already locked. #### Example Usage ```python from canvas_sdk.effects.note.note import Note from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): note_effect = Note(instance_id="existing-note-uuid") return [note_effect.lock()] ``` > **Info:** This effect will be originated by the current actor that triggered the event, with a fallback to Canvas Bot if no actor is found. #### Validating before a note is locked or signed To conditionally allow or block a note from being locked or signed — for example, requiring that a specific command or CPT code is present, that the signer is on the patient's care team, or that the signer holds a matching state license — use the [`NOTE_STATE_CHANGE_EVENT_PRE_CREATE`](/sdk/effect-event-validation-error/) validation-error effect. When the validation fails, the effect prevents the state change and surfaces an error to the user. See the [`pre-lock-validation` example plugin](https://github.com/Medical-Software-Foundation/canvas/tree/main/extensions/pre-lock-validation/pre_lock_validation). For example, to block a lock until the note has a billing line item: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.validation import EventValidationError from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data.billing import BillingLineItem, BillingLineItemStatus class RequireCptBeforeLock(BaseHandler): RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_PRE_CREATE) def compute(self) -> list[Effect]: if self.event.context.get("state") != "LKD": return [] has_line_item = BillingLineItem.objects.filter( note__id=self.event.context["note_id"], status=BillingLineItemStatus.ACTIVE, ).exists() if has_line_item: return [] error = EventValidationError() error.add_error("Add a CPT code before locking this note.") return [error.apply()] ``` ### Sign `sign() → Effect` Signs an existing note, marking it as reviewed and approved by the provider. Has the exact same effect as clicking on the `Sign` button in the Note footer. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of the note to sign| Yes **Note** : `instance_id` must be a valid, existing Note that is not already signed. > **Warning:** A note must be **locked before it can be signed**. Only certain note state transitions are allowed, and `SGN` is not reachable directly from `ULK` or `CVD` — calling `sign()` on an unlocked note raises `ValueError: Invalid state transition`. The Canvas UI combines both actions into the `Sign` button (which is why you'll see two note-state-change events in the audit history). A plugin must do the same and sequence a `lock()` effect before the `sign()` effect. #### Example Usage ```python from canvas_sdk.effects.note.note import Note from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): note_effect = Note(instance_id="existing-note-uuid") return [note_effect.sign()] ``` To lock and sign a note in a single plugin action, return the `lock()` effect before the `sign()` effect: ```python # inside compute() note = Note(instance_id="existing-note-uuid") return [note.lock(), note.sign()] ``` > **Info:** This effect will be originated by the current actor that triggered the event, with a fallback to Canvas Bot if no actor is found. ### Unlock `unlock() → Effect` Unlocks a previously locked/signed note, allowing modifications again. Has the exact same effect as clicking on the `Unlock/Amend` button in the Note footer. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of the note to unlock| Yes **Note** : `instance_id` must be a valid, existing Note that is currently locked. #### Example Usage ```python from canvas_sdk.effects.note.note import Note from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): note_effect = Note(instance_id="existing-note-uuid") return [note_effect.unlock()] ``` > **Info:** This effect will be originated by the current actor that triggered the event, with a fallback to Canvas Bot if no actor is found. ### Check In `check_in() → Effect` Marks a patient as checked in for their appointment. Has the exact same effect as clicking on the `Check In` button in the Appointment note. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of the note for check-in| Yes **Note** : `instance_id` must be a valid, existing Note associated with an appointment. #### Example Usage ```python from canvas_sdk.effects.note.note import Note from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): note_effect = Note(instance_id="existing-note-uuid") return [note_effect.check_in()] ``` > **Info:** This effect will be originated by the current actor that triggered the event, with a fallback to Canvas Bot if no actor is found. ### No Show `no_show() → Effect` Marks an appointment as a no-show when the patient does not arrive. Has the exact same effect as marking an appointment as `No Show` in the Appointment note. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of the note to mark no-show| Yes **Note** : `instance_id` must be a valid, existing Note associated with an appointment. #### Example Usage ```python from canvas_sdk.effects.note.note import Note from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): note_effect = Note(instance_id="existing-note-uuid") return [note_effect.no_show()] ``` > **Info:** This effect will be originated by the current actor that triggered the event, with a fallback to Canvas Bot if no actor is found. ### Delete `delete() → Effect` Deletes an existing note. Has the exact same effect as clicking on the `Delete` button in the Note footer. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of the note to delete| Yes **Note** : `instance_id` must be a valid, existing Note whose current state allows deletion (e.g. `NEW`, `CONVERTED`, `UNLOCKED`, `PUSHED`, or `UNDELETED`). #### Example Usage ```python from canvas_sdk.effects.note.note import Note from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): note_effect = Note(instance_id="existing-note-uuid") return [note_effect.delete()] ``` > **Info:** This effect will be originated by the current actor that triggered the event, with a fallback to Canvas Bot if no actor is found. ### Undelete `undelete() → Effect` Restores a previously deleted note. Has the exact same effect as clicking on the `Restore` button on a deleted note. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of the note to restore| Yes **Note** : `instance_id` must be a valid, existing Note that is currently in the `DELETED` state. #### Example Usage ```python from canvas_sdk.effects.note.note import Note from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): note_effect = Note(instance_id="existing-note-uuid") return [note_effect.undelete()] ``` > **Info:** This effect will be originated by the current actor that triggered the event, with a fallback to Canvas Bot if no actor is found. ### Discharge `discharge() → Effect` Locks and discharges an inpatient note. Has the exact same effect as clicking on the `Lock and discharge` button in the Inpatient note footer. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of the inpatient note to discharge| Yes **Note** : `instance_id` must be a valid, existing Note whose `NoteTypeVersion.category` is `INPATIENT`, and whose current state allows discharge (`NEW`, `CONVERTED`, `UNLOCKED`, or `UNDELETED`). #### Example Usage ```python from canvas_sdk.effects.note.note import Note from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): note_effect = Note(instance_id="existing-inpatient-note-uuid") return [note_effect.discharge()] ``` > **Info:** This effect will be originated by the current actor that triggered the event, with a fallback to Canvas Bot if no actor is found. ### Upsert Metadata `upsert_metadata(key: str, value: str) → Effect` Creates or updates a metadata entry for the specified note. For detailed documentation on note metadata management, see [NoteMetadata Effect](/sdk/effect-note-metadata/). #### Parameters Parameter| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of the note (set on the `Note` effect)| Yes `key`| `str`| Unique identifier for the metadata entry within the note context| Yes `value`| `str`| The metadata value to store| Yes #### Example Usage ```python from canvas_sdk.effects.note.note import Note from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): note = Note(instance_id="existing-note-uuid") return [note.upsert_metadata(key="my_plugin:custom_key", value="custom_value")] ``` ## ScheduleEvent Effect The `ScheduleEvent` effect enables creating, updating, and deleting schedule events for providers, with optional patient association. Build a `ScheduleEvent` with the attributes the operation needs, then return one of its methods from your handler: `create()`, covered here, or `update()`, `reschedule()`, or `delete()`. `create() → Effect` creates a schedule event with the attributes below. > **Info:** Canvas doesn't check the provider's availability when a plugin creates, updates, or reschedules a schedule event. The double booking setting and [Google Calendar Sync](/product-updates/google-calendar-sync/) busy time don't block the effect, so check for conflicts in your plugin before booking. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `note_type_id`| `UUID` or `str`| Identifier for the note type (must be of category `SCHEDULE_EVENT`)| Yes `patient_id`| `str` or `None`| Identifier for the patient (if applicable)| Conditional `description`| `str` or `None`| Custom description for the event| Conditional `start_time`| `datetime.datetime`| Start time of the event| Yes `duration_minutes`| `int`| Duration of the event in minutes| Yes `practice_location_id`| `UUID` or `str`| Identifier for the practice location| Yes `provider_id`| `str`| Identifier for the provider| Yes `status`| `AppointmentProgressStatus` or `None`| Status of the event| No `external_identifiers`| `list[AppointmentIdentifier]` or `None`| External system identifiers| No ### Implementation Details - Validates that the note type exists and is of category `SCHEDULE_EVENT` - Ensures patient is provided if the note type requires it - Verifies that custom descriptions are only used for note types that allow them - Validates that the practice location and provider exist ### Example Usage ```python import datetime from canvas_sdk.effects.note.appointment import ScheduleEvent from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): schedule_event_effect = ScheduleEvent( note_type_id="schedule-event-note-type-uuid", patient_id="patient-uuid", # Optional depending on note type description="Team meeting", # Optional depending on note type start_time=datetime.datetime.now(), duration_minutes=30, practice_location_id="practice-location-uuid", provider_id="provider-uuid" ) return [schedule_event_effect.create()] ``` ### Update Schedule Event `update() → Effect` Updates an existing schedule event in place. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of the event to update| Yes `start_time`| `datetime.datetime`| New start time| No `duration_minutes`| `int`| New duration in minutes| No `description`| `str` or `None`| Updated description| No `practice_location_id`| `UUID` or `str`| New practice location| No `provider_id`| `str`| New provider| No `status`| `AppointmentProgressStatus` or `None`| Updated status| No #### Example Usage ```python import datetime from canvas_sdk.effects.note import AppointmentIdentifier from canvas_sdk.effects.note.appointment import ScheduleEvent from canvas_sdk.effects.note.base import AppointmentIdentifier from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): schedule_event_effect = ScheduleEvent(instance_id="existing-event-uuid") schedule_event_effect.start_time = datetime.datetime.now() + datetime.timedelta(days=1) schedule_event_effect.duration_minutes = 60 schedule_event_effect.description = "Rescheduled team meeting" schedule_event_effect.external_identifiers = [ AppointmentIdentifier(system="test_system", value="123TEST") ] return [schedule_event_effect.update()] ``` ### Reschedule Schedule Event `reschedule() → Effect` Reschedules an existing schedule event by creating a new event and cancelling the original. This maintains the event history and ensures proper tracking of rescheduled events. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of the event to reschedule| Yes `start_time`| `datetime.datetime`| New start time| No `duration_minutes`| `int`| New duration in minutes| No `description`| `str` or `None`| Updated description| No `practice_location_id`| `UUID` or `str`| New practice location| No `provider_id`| `str`| New provider| No `status`| `AppointmentProgressStatus` or `None`| Updated status| No `external_identifiers`| `list[AppointmentIdentifier]` or `None`| Updated external identifiers| No **Note** : At least one field (besides `instance_id`) must be modified. #### Example Usage ```python import datetime from canvas_sdk.effects.note.appointment import ScheduleEvent from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): schedule_event_effect = ScheduleEvent(instance_id="existing-event-uuid") schedule_event_effect.start_time = datetime.datetime.now() + datetime.timedelta(hours=3) schedule_event_effect.duration_minutes = 45 return [schedule_event_effect.reschedule()] ``` ### Delete Schedule Event `delete() → Effect` Marks a schedule event as cancelled. #### Example Usage ```python import datetime from canvas_sdk.effects.note.appointment import ScheduleEvent from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): schedule_event_effect = ScheduleEvent(instance_id="existing-event-uuid") return [schedule_event_effect.delete()] ``` * * * ## Appointment Effect The `Appointment` effect facilitates creating, updating, and cancelling patient appointments with providers. Build an `Appointment` with the attributes the operation needs, then return one of its methods from your handler: `create()`, covered here, or `update()`, `reschedule()`, `cancel()`, or `revert()`. `create() → Effect` creates an appointment with the attributes below. > **Info:** Canvas doesn't check the provider's availability when a plugin creates, updates, or reschedules an appointment. The double booking setting and [Google Calendar Sync](/product-updates/google-calendar-sync/) busy time don't block the effect, so check for conflicts in your plugin before booking. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `appointment_note_type_id`| `UUID` or `str`| Identifier for the appointment note type (must be of category `ENCOUNTER` and scheduleable)| Yes `patient_id`| `str`| Identifier for the patient| Yes `meeting_link`| `str` or `None`| Link for virtual appointments| No `start_time`| `datetime.datetime`| Start time of the appointment| Yes `duration_minutes`| `int`| Duration of the appointment in minutes| Yes `practice_location_id`| `UUID` or `str`| Identifier for the practice location| Yes `provider_id`| `str`| Identifier for the provider| Yes `status`| `AppointmentProgressStatus` or `None`| Status of the appointment| No `external_identifiers`| `list[AppointmentIdentifier]` or `None`| External system identifiers| No ### Implementation Details - Validates that the appointment note type exists, is of category `ENCOUNTER`, and is scheduleable - Ensures the patient exists in the system - Verifies that the practice location and provider exist - Records one `SCH` (Scheduling) and one `BKD` (Booked) entry in the appointment note's [state history](/sdk/data-note/#notestatechangeevent). Both entries have the user who triggered the effect as their `originator`, or Canvas Bot if no user triggered it. The note's own `originator` is the appointment's provider. ### Example Usage ```python import datetime from canvas_sdk.effects.note.appointment import Appointment from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): appointment_effect = Appointment( appointment_note_type_id="appointment-note-type-uuid", patient_id="patient-uuid", meeting_link="https://zoom.us/example-link", # Optional start_time=datetime.datetime.now(), duration_minutes=60, practice_location_id="practice-location-uuid", provider_id="provider-uuid" ) return appointment_effect.create() ``` ### Update Appointment `update() → Effect` Updates an existing appointment in place. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of appointment to update| Yes `start_time`| `datetime.datetime`| New start time| No `duration_minutes`| `int`| New duration in minutes| No `meeting_link`| `str` or `None`| Updated meeting link| No `practice_location_id`| `UUID` or `str`| New practice location| No `provider_id`| `str`| New provider| No `status`| `AppointmentProgressStatus` or `None`| Updated status| No `external_identifiers`| `list[AppointmentIdentifier]` or `None`| Updated external identifiers| No **Note** : `patient_id` cannot be updated after creation. #### Example Usage ```python import datetime from canvas_sdk.effects.note import AppointmentIdentifier from canvas_sdk.effects.note.appointment import Appointment from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): appointment_effect = Appointment(instance_id="existing-appointment-uuid") appointment_effect.start_time = datetime.datetime.now() + datetime.timedelta(hours=2) appointment_effect.duration_minutes = 45 appointment_effect.meeting_link = "https://new-meeting-link.com" appointment_effect.external_identifiers = [ AppointmentIdentifier(system="my_external_system", value="appt-12345") ] return appointment_effect.update() ``` ### Reschedule Appointment `reschedule() → Effect` Reschedules an existing appointment by creating a new appointment and cancelling the original. This maintains the appointment history and ensures proper tracking of rescheduled appointments. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of appointment to reschedule| Yes `start_time`| `datetime.datetime`| New start time| No `duration_minutes`| `int`| New duration in minutes| No `meeting_link`| `str` or `None`| Updated meeting link| No `practice_location_id`| `UUID` or `str`| New practice location| No `provider_id`| `str`| New provider| No `status`| `AppointmentProgressStatus` or `None`| Updated status| No `external_identifiers`| `list[AppointmentIdentifier]` or `None`| Updated external identifiers| No **Note** : At least one field (besides `instance_id`) must be modified. `patient_id` cannot be updated after creation. Rescheduling adds a `BKD` (Booked) entry to the appointment note's [state history](/sdk/data-note/#notestatechangeevent). The entry has the user who triggered the effect as its `originator`, or Canvas Bot if no user triggered it. #### Example Usage ```python import datetime from canvas_sdk.effects.note.appointment import Appointment from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): appointment_effect = Appointment(instance_id="existing-appointment-uuid") appointment_effect.start_time = datetime.datetime.now() + datetime.timedelta(days=1) appointment_effect.duration_minutes = 60 return appointment_effect.reschedule() ``` ### Cancel Appointment `cancel() → Effect` Cancels an existing appointment and updates its status. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of the appointment to cancel| Yes **Note** : `instance_id` must be a valid, existing Appointment whose current state allows cancellation. An appointment can only be cancelled when it is in the `BOOKED` or `REVERTED` state. #### Example Usage ```python from canvas_sdk.effects.note.appointment import Appointment from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): appointment_effect = Appointment(instance_id="existing-appointment-uuid") return appointment_effect.cancel() ``` ### Revert Appointment `revert() → Effect` Reverts a booked or checked-in appointment back to a state where it can be checked in, cancelled, rescheduled, or marked as no-show. #### Attributes Attribute| Type| Description| Required ---|---|---|--- `instance_id`| `UUID` or `str`| Identifier of the appointment to revert| Yes **Note** : `instance_id` must be a valid, existing Appointment whose current state allows reversion. An appointment can only be reverted when it is in the `CANCELLED`, `CONVERTED`, or `NOSHOW` state. #### Example Usage ```python from canvas_sdk.effects.note.appointment import Appointment from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): appointment_effect = Appointment(instance_id="existing-appointment-uuid") return appointment_effect.revert() ``` ## Managing Appointment Labels Canvas supports adding up to 3 labels per appointment for categorization and workflow automation. Labels can be managed programmatically using the appointment label effects. For detailed documentation on appointment label management, see [Appointment Label Effects](/sdk/effect-appointment-labels/). ### Quick Example ```python from canvas_sdk.effects.note.appointment import AddAppointmentLabel, RemoveAppointmentLabel from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.APPOINTMENT_LABEL_ADDED), EventType.Name(EventType.APPOINTMENT_LABEL_REMOVED)] def compute(self): # Add labels to an appointment add_effect = AddAppointmentLabel( appointment_id="appointment-uuid", labels={"URGENT", "FOLLOW_UP"} ) # Remove labels from an appointment remove_effect = RemoveAppointmentLabel( appointment_id="appointment-uuid", labels={"CANCELLED"} ) return [add_effect.apply(), remove_effect.apply()] ``` * * * ## Validation All effects perform comprehensive validation before execution: 1. **Entity Existence** : Validates that referenced entities (patients, providers, practice locations, note types) exist in the system 2. **Type Compatibility** : Ensures note types are appropriate for the intended operation: - Visit notes cannot use `APPOINTMENT`, `SCHEDULE_EVENT`, `MESSAGE`, or `LETTER` note types - Schedule events must use `SCHEDULE_EVENT` note types - Appointments must use `ENCOUNTER` note types that are scheduleable 3. **Field Requirement Enforcement** : The system validates conditional field requirements based on note type configurations: - **Patient Association Requirements** : For note types with `is_patient_required=True`, the system enforces that a valid patient ID is provided. This is particularly important for schedule events that may or may not be associated with specific patients. - **Custom Description Validation** : When a note type has `allow_custom_title=False`, the system prevents custom descriptions from being added. This ensures adherence to standardized naming conventions for certain types of appointments and events. - **Required Field Validation** : All required fields are checked for proper values and formats before the effect is executed. 4. **Update Restrictions** : Certain fields cannot be modified after creation: - **Notes** : `patient_id` and `note_type_id` are immutable - **Appointments** : `patient_id` is immutable - **All Effects** : At least one field must be modified for an update operation to succeed --- # Observation Effect Source: https://docs.canvasmedical.com/sdk/effect-observation/ The `Observation` effect provides a unified way to create and update clinical observations within the Canvas platform. Observations can include vitals (blood pressure, temperature, etc.), lab results, and other clinical measurements. The effect supports structured coding using standard terminologies (LOINC, SNOMED), components for multi-part measurements, and value codings for interpretation. Build an `Observation` with the attributes you want to set, then return one of its methods from your handler: `create()`, `update()`, or `enter_in_error()`. ## Methods The examples below share this setup: ```python import datetime from canvas_sdk.effects.observation import Observation, CodingData, ObservationComponentData from canvas_sdk.v1.data.observation import Observation as ObservationModel from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.first() ``` ### create() → Effect Create a new observation record. #### Validation - `observation_id` must **not** be set (will be generated by the system) - `patient_id` is **required** - `name` is **required** - `effective_datetime` is **required** - If `is_member_of_id` is provided, the parent observation must exist #### Example: Blood Pressure Observation ```python # Create a blood pressure observation with components and codings bp_observation = Observation( patient_id=patient.id, name="Blood Pressure", category="vital-signs", value="120/80", units="mmHg", effective_datetime=datetime.datetime.now(), codings=[ CodingData( code="85354-9", display="Blood pressure panel with all children optional", system="http://loinc.org", version="2.73", user_selected=True, ) ], components=[ ObservationComponentData( value_quantity="120", value_quantity_unit="mmHg", name="Systolic Blood Pressure", codings=[ CodingData( code="8480-6", display="Systolic blood pressure", system="http://loinc.org", ) ], ), ObservationComponentData( value_quantity="80", value_quantity_unit="mmHg", name="Diastolic Blood Pressure", codings=[ CodingData( code="8462-4", display="Diastolic blood pressure", system="http://loinc.org", ) ], ), ], value_codings=[ CodingData( code="normal", display="Normal", system="http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", ) ], ) effect_create = bp_observation.create() ``` #### Example: Simple Lab Result ```python # Create a simple lab observation glucose = Observation( patient_id=patient.id, name="Glucose", category="laboratory", value="95", units="mg/dL", effective_datetime=datetime.datetime.now(), codings=[ CodingData( code="2339-0", display="Glucose [Mass/volume] in Blood", system="http://loinc.org", ) ], ) effect_create_lab = glucose.create() ``` #### Example: Multiple Categories ```python # Create an observation that belongs to multiple categories comprehensive_assessment = Observation( patient_id=patient.id, name="Comprehensive Physical Assessment", category=["vital-signs", "exam"], # Multiple categories value="Normal", effective_datetime=datetime.datetime.now(), codings=[ CodingData( code="29545-1", display="Physical examination", system="http://loinc.org", ) ], ) effect_create_multi = comprehensive_assessment.create() ``` ### update() → Effect Update an existing observation. - Only fields marked dirty (modified on the model) are included in the update. #### Validation - `observation_id` is **required** and must reference an existing observation - All other fields are optional; only dirty (modified) fields are updated - If `is_member_of_id` is provided, the parent observation must exist #### Example ```python # Find an existing observation existing_obs = ObservationModel.objects.filter(patient_id=patient.id).first() # Update the blood pressure values updated_bp = Observation( observation_id=existing_obs.id, value="130/85", units="mmHg", components=[ ObservationComponentData( value_quantity="130", value_quantity_unit="mmHg", name="Systolic Blood Pressure", codings=[ CodingData( code="8480-6", display="Systolic blood pressure", system="http://loinc.org", ) ], ), ObservationComponentData( value_quantity="85", value_quantity_unit="mmHg", name="Diastolic Blood Pressure", codings=[ CodingData( code="8462-4", display="Diastolic blood pressure", system="http://loinc.org", ) ], ), ], ) effect_update = updated_bp.update() ``` ### enter_in_error() → Effect Marks an existing observation as entered in error. Use this when an observation was recorded incorrectly and should be flagged rather than deleted. - Only `observation_id` is allowed; setting any other field will raise a validation error. #### Validation - `observation_id` is **required** and must reference an existing observation - All other fields must **not** be set (only `observation_id` is allowed) - The observation must not already be entered in error - The observation must not belong to a locked note #### Example ```python # Find an observation that was recorded incorrectly erroneous_obs = ObservationModel.objects.filter(patient_id=patient.id).first() # Mark it as entered in error error_observation = Observation(observation_id=erroneous_obs.id) effect_error = error_observation.enter_in_error() ``` ## Attributes Name| Type| Description| Required ---|---|---|--- `observation_id`| `str` or `UUID` or `None`| Unique identifier of an existing observation. Must be unset when creating; required when updating.| For `update()` and `enter_in_error()` `patient_id`| `str` or `None`| ID of the patient for this observation. Required when creating.| For `create()` `is_member_of_id`| `str` or `UUID` or `None`| Reference to a parent observation (for grouping related observations).| No `category`| `str` or `list[str]` or `None`| Category of observation (e.g., "vital-signs", "laboratory", "imaging"). Can be a single category or a list of categories.| No `units`| `str` or `None`| Unit of measure for the observation value (e.g., "mmHg", "mg/dL").| No `value`| `str` or `None`| The observation value as a string.| No `note_id`| `int` or `None`| ID of the note associated with this observation.| No `name`| `str` or `None`| Human-readable name for the observation. Required when creating.| For `create()` `effective_datetime`| `datetime` or `None`| Date and time when the observation was taken. Required when creating.| For `create()` `codings`| `list[CodingData]` or `None`| List of standardized codes identifying this observation (e.g., LOINC codes).| No `components`| `list[ObservationComponentData]` or `None`| List of components for multi-part observations (e.g., systolic and diastolic BP).| No `value_codings`| `list[CodingData]` or `None`| List of coded values for interpretation (e.g., "normal", "abnormal").| No ## Helper Classes ### `CodingData` Represents a standardized code from a terminology system (LOINC, SNOMED, etc.). Name| Type| Description ---|---|--- `code`| `str`| The code value from the terminology system. `display`| `str`| Human-readable display text for the code. `system`| `str`| URI identifying the terminology system (e.g., "http://loinc.org"). `version`| `str`| Version of the terminology system. Defaults to empty string. `user_selected`| `bool`| Whether this code was explicitly selected by the user. Defaults to False. ### `ObservationComponentData` Represents a component of a multi-part observation (e.g., systolic and diastolic blood pressure). Name| Type| Description ---|---|--- `value_quantity`| `str`| The numeric value of this component. `value_quantity_unit`| `str`| Unit of measure for this component value. `name`| `str`| Name of this component. `codings`| `list[CodingData]` or `None`| Standardized codes identifying this component. --- # Patient Facility Address Source: https://docs.canvasmedical.com/sdk/effect-patient-facility-address/ The `PatientFacilityAddress` effect enables the creation, updating, and deletion of patient facility address records within Canvas. Patient facility addresses link patients to healthcare facilities, with optional room number information. The address details are automatically populated from the linked facility. You can either reference an existing facility by ID, or create a new facility inline by providing the facility details. Build a `PatientFacilityAddress` with the attributes you want to set, then return one of its methods from your handler: `create()`, `update()`, or `delete()`. ## Methods ### create() → Effect Creates a patient facility address. - `patient_id` is required, and must be the id of an existing patient. - Supply either `facility_id` for an existing facility, or the fields to create a new facility (`facility_name`, `facility_city`, `facility_state_code`, and `facility_postal_code`), but not both. See Facility Reference Options. - `id` must not be set. ### update() → Effect Updates an existing patient facility address. - `id` is required, and must be the id of an existing patient facility address. ### delete() → Effect Deletes an existing patient facility address. - `id` is required, and must be the id of an existing patient facility address. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `id`| `str` or `UUID`| ID of the patient facility address| For `update()` and `delete()` `patient_id`| `str` or `UUID`| ID of the patient| For `create()` `facility_id`| `str` or `UUID`| ID of an existing facility to link| For `create()`, unless creating a new facility `facility_name`| `str`| Name of new facility to create| When creating a new facility `facility_npi_number`| `str`| NPI number for new facility| No `facility_phone_number`| `str`| Phone number for new facility| No `facility_fax_number`| `str`| Fax number for new facility| No `facility_active`| `bool`| Whether the new facility is active| No `facility_line1`| `str`| Street address line 1 for new facility| No `facility_line2`| `str`| Street address line 2 for new facility| No `facility_city`| `str`| City for new facility| When creating a new facility `facility_district`| `str`| District for new facility| No `facility_state_code`| `str`| State code for new facility (e.g., "CA", "NY")| When creating a new facility `facility_postal_code`| `str`| Postal code for new facility| When creating a new facility `room_number`| `str`| Room number at the facility| No `address_type`| `AddressType` or `str`| Type of address: "physical" or "both". Defaults to "physical".| No ## Facility Reference Options When creating a patient facility address, you must either: 1. **Reference an existing facility** by providing `facility_id` 2. **Create a new facility inline** by providing facility creation fields (`facility_name`, `facility_city`, `facility_state_code`, `facility_postal_code`) > **Warning:** You cannot specify both `facility_id` and facility creation fields. Use one approach or the other. ### Required Fields for Inline Facility Creation When creating a new facility inline, the following fields are required: - `facility_name` - `facility_city` - `facility_state_code` - `facility_postal_code` ## Address Type The `address_type` field accepts the following values: Value| Description ---|--- `physical`| Physical/street address (default) `both`| Both physical and mailing address ## Validation The effect validates: - **Create** : `patient_id` is required and must reference an existing patient - **Create** : Either `facility_id` or facility creation fields must be provided (not both) - **Create** : If `facility_id` is provided, it must reference an existing facility - **Create** : If creating a new facility, all required facility fields must be provided - **Update/Delete** : `id` is required and must reference an existing patient facility address - **Update** : If updating facility, same rules apply as create (facility_id OR creation fields) - `address_type` must be "physical" or "both" if provided ## Example Usage ### Creating with Existing Facility ```python from canvas_sdk.effects.patient_facility_address import PatientFacilityAddress, AddressType from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): effect = PatientFacilityAddress( patient_id="patient-uuid-here", facility_id="facility-uuid-here", room_number="101A", address_type=AddressType.PHYSICAL, ) return [effect.create()] ``` ### Creating with New Facility ```python from canvas_sdk.effects.patient_facility_address import PatientFacilityAddress, AddressType from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): effect = PatientFacilityAddress( patient_id="patient-uuid-here", facility_name="Downtown Medical Center", facility_line1="123 Main Street", facility_line2="Suite 400", facility_city="Boston", facility_state_code="MA", facility_postal_code="02101", facility_phone_number="617-555-1234", facility_npi_number="1234567890", room_number="Room 205", address_type=AddressType.PHYSICAL, ) return [effect.create()] ``` ### Updating an Existing Address ```python from canvas_sdk.effects.patient_facility_address import PatientFacilityAddress from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): # Update to use a different existing facility effect = PatientFacilityAddress( id="existing-address-uuid", facility_id="new-facility-uuid", room_number="202B", ) return [effect.update()] ``` ### Deleting an Address ```python from canvas_sdk.effects.patient_facility_address import PatientFacilityAddress from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): effect = PatientFacilityAddress( id="existing-address-uuid", ) return [effect.delete()] ``` ## Notes - The address details (line1, line2, city, state, country, postal_code) displayed for a patient facility address are automatically populated from the linked facility's address information. - When a facility's address is updated, all linked patient facility addresses are automatically updated to match. This synchronization happens asynchronously and applies to line1, line2, city, district, state_code, postal_code, and country fields. Non-address changes to the facility (such as name, NPI number, or phone number) do not trigger this cascade. - When creating a new facility inline, the facility is created first, then linked to the patient facility address. - Room number is optional and can be used to specify the patient's specific room within the facility. --- # Patient Group Source: https://docs.canvasmedical.com/sdk/effect-patient-group/ The Canvas SDK provides effects for managing patient membership in groups. These effects are idempotent — adding a patient who is already a member or deactivating a patient who is not an active member will have no effect. ## PatientGroupEffect An effect class for performing actions on a patient group. Instantiate it with a `group_id`, then call methods to add or deactivate members. ### Methods #### add_member(patient_ids: list[str]) → Effect Ensures one or more patients are members of the group. Parameter| Type| Description ---|---|--- `patient_ids`| `list[str]`| List of [patient](/sdk/data-patient/) ids to add to the group #### deactivate_member(patient_ids: list[str]) → Effect Ensures one or more patients are not active members of the group. If a patient is currently locked in the group, this effect will be ignored for that patient. Parameter| Type| Description ---|---|--- `patient_ids`| `list[str]`| List of [patient](/sdk/data-patient/) ids to deactivate from the group ### Attributes Attribute| Type| Description| Required ---|---|---|--- `group_id`| `UUID`| The id of the [patient group](/sdk/data-patient-group/)| Yes ### Example Usage ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.patient_group import PatientGroupEffect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data import Patient, PatientGroup class AddMemberHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.PATIENT_UPDATED) def compute(self) -> list[Effect]: """Add patients to a group.""" patient = Patient.objects.get(id=self.target) group = PatientGroup.objects.first() effect = PatientGroupEffect(group_id=str(group.id)) return [effect.add_member(patient_ids=[str(patient.id)])] class DeactivateMemberHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.PATIENT_UPDATED) def compute(self) -> list[Effect]: """Deactivate a patient from a group.""" patient = Patient.objects.get(id=self.target) group = PatientGroup.objects.first() effect = PatientGroupEffect(group_id=str(group.id)) return [effect.deactivate_member(patient_ids=[str(patient.id)])] ``` --- # Patient Identification Card Effect Source: https://docs.canvasmedical.com/sdk/effect-patient-identification-card/ The `PatientIdentificationCard` effect adds, changes, or deletes an image of a patient's identification card, such as a driver's license, from a plugin. Use it to collect ID images during intake or registration. To read existing cards, see [PatientIdentificationCard](/sdk/data-patient/#patientidentificationcard) on the patient data model. Build a `PatientIdentificationCard` with the attributes you want to set, then return one of its methods from your handler: `create()`, `update()`, or `delete()`. ## Methods ### create() → Effect Creates an identification card for a patient. - `patient_id` and `image_upload_key` are required. - `card_id` must not be set. ### update() → Effect Changes an existing identification card. - `card_id` is required, and must be the `dbid` of an existing card. - Only the attributes you set on the effect are changed. - `patient_id` cannot be changed, and `active` cannot be set to `None`. ### delete() → Effect Deletes an existing identification card. `card_id` is required. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `card_id`| `int`| The `dbid` of an existing [PatientIdentificationCard](/sdk/data-patient/#patientidentificationcard). Cards have no UUID, so they are identified by `dbid`. Must be unset when creating a card.| For `update()` and `delete()` `patient_id`| `str`| The `id` of the [Patient](/sdk/data-patient/#patient) the card belongs to.| For `create()` `image_upload_key`| `str`| The upload key of the card image. See Card image.| For `create()` `title`| `str` or `None`| A label for the card, such as `"Driver license"`. Up to 255 characters.| No `active`| `bool`| Whether the card is active.| No ## Card image The card image is a PNG or JPEG file that a user uploads to your plugin through a SimpleAPI route declared with `file_uploads="stored"`. Set `image_upload_key` to the `key` of the uploaded `StoredFilePart`. See [Storing uploaded files](/sdk/handlers-simple-api-http/#storing-uploaded-files). The key must come from a file your plugin uploaded, and the file must be a PNG or JPEG image. When the effect is applied, Canvas copies the file to the card and then deletes the upload, so each key works only once. On `update()`, setting `image_upload_key` replaces the card's image, and a key of `None` keeps the current image. For working code, see Upload an ID card from a form. ## Examples ### Upload an ID card from a form This SimpleAPI route creates an identification card from a `multipart/form-data` request that carries the patient's id, a title, and a photo of the card. The route uses the [API key mixin](/sdk/handlers-simple-api-http/#api-key-1), so declare the `simpleapi-api-key` secret in your manifest and set it on your instance: ```python from http import HTTPStatus from canvas_sdk.effects import Effect from canvas_sdk.effects.patient import PatientIdentificationCard from canvas_sdk.effects.simple_api import JSONResponse, Response from canvas_sdk.handlers.simple_api import APIKeyAuthMixin, SimpleAPI, api class IdentificationCardAPI(APIKeyAuthMixin, SimpleAPI): @api.post("/identification-card", file_uploads="stored") def identification_card(self) -> list[Response | Effect]: form = self.request.form_data() image = form["card_image"] if image.error: return [JSONResponse({"error": "upload failed"}, status_code=HTTPStatus.BAD_GATEWAY)] card = PatientIdentificationCard( patient_id=form["patient_id"].value, image_upload_key=image.key, title=form["title"].value, ) return [card.create(), JSONResponse({}, status_code=HTTPStatus.ACCEPTED)] ``` Call the route with a `multipart/form-data` POST. Each form field name matches a key the handler reads, the card photo is sent as a file, and the API key goes in the `Authorization` header: ```shell curl --request POST \ --url 'https://.canvasmedical.com/plugin-io/api//identification-card' \ --header 'Authorization: ' \ --form 'patient_id=b80b1cdc2e6a4aca90ccebc02e683f35' \ --form 'title=Driver license' \ --form 'card_image=@license.jpg;type=image/jpeg' ``` The same request from a Python script, uploading a photo saved on disk, using the [`requests`](https://requests.readthedocs.io/) library: ```python from pathlib import Path import requests url = "https://.canvasmedical.com/plugin-io/api//identification-card" image_path = Path("license.jpg") with image_path.open("rb") as image: response = requests.post( url, headers={"Authorization": ""}, data={ "patient_id": "b80b1cdc2e6a4aca90ccebc02e683f35", "title": "Driver license", }, files={"card_image": (image_path.name, image, "image/jpeg")}, timeout=30, ) print(response.status_code) ``` The route answers: - `202 Accepted` when the photo was stored and the effect was returned. - `502 Bad Gateway` when the photo failed to store. - `400 Bad Request` when the request isn't `multipart/form-data`. - `401 Unauthorized` when the API key is missing or wrong. ### Deactivate or delete a card ```python from canvas_sdk.effects.patient import PatientIdentificationCard from canvas_sdk.v1.data.patient import Patient patient = Patient.objects.get(id="d7af3e356368446c85b40a5d6ff7288e") card = patient.identification_cards.filter(active=True).first() # Deactivate a card deactivate = PatientIdentificationCard(card_id=card.dbid, active=False).update() # Delete a card delete = PatientIdentificationCard(card_id=card.dbid).delete() ``` --- # PatientMetadata Effect Source: https://docs.canvasmedical.com/sdk/effect-patient-metadata/ The `PatientMetadata` effect provides a flexible key-value storage system for patient-specific data within the Canvas system. This effect enables the creation and updating of custom metadata entries associated with patient records, allowing for extensible patient information storage beyond standard demographic fields. ## Overview Patient metadata serves as a powerful extension mechanism for storing custom patient-related information that doesn't fit within the standard patient data model. It uses the `.upsert(value)` method to apply a value to the key attributed with the Metadata effect object. > **Info:** For storing mutable custom patient fields — such as a status or risk score that is not part of the standard Patient data model — the patient metadata key-value effect is the recommended approach rather than questionnaires (a questionnaire creates a new record on each update). You can also surface these values to end users by building a custom form in the demographics section of the profile with the [Patient Metadata Create form effect](/sdk/patient-metadata-create-form-effect/), though displaying the fields is optional — a plugin can set the data without exposing a form. ## Methods ### upsert(value: str) → Effect Creates or updates a metadata entry for the specified patient and key combination. #### Parameters Parameter| Type| Description| Required ---|---|---|--- `value`| `str`| The metadata value to store| Yes #### Behavior - If a metadata entry with the specified key already exists for the patient, it will be updated with the new value - If no entry exists, a new metadata entry will be created ## Attributes Attribute| Type| Description| Required ---|---|---|--- `patient_id`| `str`| Id of the [Patient](/sdk/data-patient/) record to associate metadata with| Yes `key`| `str`| Unique identifier for the metadata entry within the patient context. Maximum 256 characters.| Yes ## Implementation Details ### Validation The effect performs comprehensive validation before execution: 1. **Patient Existence Validation** : Verifies that the referenced patient exists in the system - Queries the patient database to confirm the `patient_id` corresponds to an existing patient record - Returns a descriptive error if the patient is not found 1. **Field Validation** : Ensures all required fields are provided and properly formatted - Both `patient_id` and `key` must be non-empty strings - The `value` parameter in the `upsert` method must be provided ### Data Structure The effect payload is structured as JSON with the following schema: ```json { "data": { "patient_id": "patient-id", "key": "metadata-key", "value": "metadata-value" } } ``` ## Example Usage ### Basic Usage ```python from canvas_sdk.effects.patient_metadata import PatientMetadata # Create a metadata entry for patient preferences metadata = PatientMetadata( patient_id="550e8400e29b41d4a716446655440000", key="preferred_contact_time" ) # Upsert the metadata value effect = metadata.upsert("morning") ``` ### Metadata Parsing Example ```python import json import re from canvas_sdk.effects.patient_metadata import PatientMetadata from canvas_sdk.handlers import BaseHandler from canvas_sdk.events import EventType class NarrativeMetadataExtractor(BaseHandler): """ Extracts structured metadata from clinical narratives. """ RESPONDS_TO = EventType.Name(EventType.PLAN_COMMAND__POST_UPDATE) def compute(self): patient_id = self.event.context["patient"]["id"] narrative = self.event.context.get("fields", {}).get("narrative", "") # Extract key-value pairs from narrative text # Pattern: key=somekey*value=somevalue key_match = re.search(r'key=([^*#_\s]+)', narrative) value_match = re.search(r'value=([^*#_\s]+)', narrative) if not (key_match and value_match): return [] key = key_match.group(1) value = value_match.group(1) # Create metadata effect metadata = PatientMetadata( patient_id=patient_id, key=key ) return [metadata.upsert(value)] ``` ## Best Practices ### Key Naming Conventions 1. **Use Descriptive Names** : Choose keys that clearly indicate the purpose of the metadata - Good: `external_mrn`, `preferred_pharmacy_id`, `risk_score_diabetes` - Avoid: `data1`, `temp`, `misc` 1. **Namespace Your Keys** : When building integrations or modules, prefix keys to avoid collisions - Example: `integration_patient_id`, `module_diabetes_last_a1c_date` ### Value Storage 1. **String Serialization** : All values are stored as strings. For complex data types: ```python # Storing JSON data import json from canvas_sdk.effects.patient_metadata import PatientMetadata metadata = PatientMetadata( patient_id="550e8400e29b41d4a716446655440000", key="result" ) complex_data = {"scores": [85, 92, 78], "average": 85.0} metadata.upsert(json.dumps(complex_data)) ``` 2. **Boolean Values** : Store as "true" or "false" strings for consistency ```python from canvas_sdk.effects.patient_metadata import PatientMetadata patient_consented = False metadata = PatientMetadata( patient_id="550e8400e29b41d4a716446655440000", key="boolean_value" ) metadata.upsert("true" if patient_consented else "false") ``` ## Notes - Metadata entries are patient-specific and isolated - the same key can have different values for different patients - There is no built-in versioning; updating a key overwrites the previous value - The system does not enforce any schema on metadata values - validation is the responsibility of the implementing code --- # Patient Timeline Source: https://docs.canvasmedical.com/sdk/effect-patient-timeline/ The Canvas SDK allows you to configure which note types a patient's chart shows and which the **New Note** button offers. Both are controlled by the `PatientTimelineEffect` class, returned in response to the `PATIENT_TIMELINE__GET_CONFIGURATION` event, which fires when a patient's chart is loaded. Build a `PatientTimelineEffect` with the attributes you want to set, then return its `apply()` from your handler. ## Methods ### apply() → Effect Applies the note type configuration to the patient's chart. No attribute is required. - Every UUID, in either attribute, must correspond to an existing [NoteType](/sdk/data-note/#notetype). A note type UUID that does not exist raises a `ValidationError` naming the note type that was not found. - Values that are not valid UUIDs also raise a `ValidationError`. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `excluded_note_types`| `list[str]`| A list of [`NoteType.unique_identifier`](/sdk/data-note/#notetype) values (UUIDs) to exclude from the patient's timeline. Defaults to `[]`.| No `allowed_new_note_types`| `list[str] \| None`| An allow-list of [`NoteType.unique_identifier`](/sdk/data-note/#notetype) values the **New Note** button may offer. `None` (the default) means no constraint; `[]` offers nothing, which hides the button. See Restricting note creation.| No The two attributes differ in scope, and you will usually want only one of them: | `excluded_note_types`| `allowed_new_note_types` ---|---|--- direction| deny-list| allow-list existing notes on the timeline| **hidden**| visible timeline's note type filter| type removed| type still offered **New Note** button| type removed| restricted to the list direct permalink to such a note| permission error| unaffected several plugins respond| **unioned**| **unioned** ## Excluding Note Types ### Example Usage The `excluded_note_types` list must contain `unique_identifier` values from the `NoteType` model. Each `NoteType` has a `unique_identifier` (UUID) that you can look up by querying the model: ```python from canvas_sdk.v1.data.note import NoteType # Find the unique_identifier for a note type by name note_type = NoteType.objects.get(name="Office visit") note_type.unique_identifier # e.g. UUID("a3b9c1d2-...") # Or list all note types with their unique_identifiers for nt in NoteType.objects.all(): print(f"{nt.name}: {nt.unique_identifier}") ``` Then use those `unique_identifier` values in the effect: ```python from canvas_sdk.effects.patient.timeline import PatientTimelineEffect from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler from canvas_sdk.v1.data.note import NoteType class MyHandler(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.PATIENT_TIMELINE__GET_CONFIGURATION)] def compute(self): # Use unique_identifier office_visit = NoteType.objects.get(name="Office visit", is_active=True) lab_visit = NoteType.objects.get(name="Lab visit", is_active=True) effect = PatientTimelineEffect( excluded_note_types=[ str(office_visit.unique_identifier), str(lab_visit.unique_identifier), ] ) return [effect.apply()] ``` ### Behavior > **Info:** **Chart Review notes cannot be excluded.** Even if a `CHART_REVIEW` note type is included in the `excluded_note_types` list, it will always be shown on the timeline. The system automatically removes it from any exclusion list. - **Permalink access** : If a user tries to directly access a note whose type has been excluded, they will receive a permission error. - **Multiple plugins** : If multiple plugins respond to the `PATIENT_TIMELINE__GET_CONFIGURATION` event, the excluded note types from all responses are combined. - **Note creation** : An excluded note type is also removed from the patient chart's **New Note** button and from the timeline's note type filter, so users cannot pick that type when creating a note. This governs what the UI offers — it does not reject a note of an excluded type created directly through the API. > **Info:** **To restrict note creation without hiding existing notes:** `excluded_note_types` hides a patient's existing notes of that type _and_ removes the type from the **New Note** button. If you only want to restrict what the button offers, while leaving the patient's history visible and filterable, use `allowed_new_note_types` instead. ## Restricting note creation `allowed_new_note_types` is an **allow-list** of the note types the **New Note** button may offer. It affects note _creation_ only: existing notes of a withheld type stay on the timeline, and the timeline's note type filter keeps offering that type, so a provider can still see and filter the history they are being stopped from adding to. A common use is limiting which note types a given provider can originate. An organization might want only certain staff sending text messages to a patient, for example: the **New Note** button offers the Message type to those roles and withholds it from everyone else, while every provider can still read the messages already on the patient's chart and filter the timeline by them. Inactive and deprecated note types are never offered, whether or not a plugin responds. The example below allow-lists by the staff member's clinical role, so a nurse can send a message or log a phone call while only a physician is offered an office visit. ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.patient.timeline import PatientTimelineEffect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data.note import NoteType from canvas_sdk.v1.data.staff import Staff ALLOWED_BY_ROLE = { "MD": ["Office visit", "Phone call", "Message"], "RN": ["Phone call", "Message"], } class RestrictNewNoteTypes(BaseHandler): RESPONDS_TO = EventType.Name(EventType.PATIENT_TIMELINE__GET_CONFIGURATION) def compute(self) -> list[Effect]: staff = Staff.objects.filter(user__dbid=self.event.actor.id).first() role = staff.top_role_abbreviation if staff else None allowed_names = ALLOWED_BY_ROLE.get(role or "", ["Message"]) note_types = NoteType.objects.filter(is_active=True, name__in=allowed_names) return [ PatientTimelineEffect( allowed_new_note_types=[str(nt.unique_identifier) for nt in note_types] ).apply() ] ``` To hide the button entirely, return an empty allow-list: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.patient.timeline import PatientTimelineEffect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class HideNewNoteButton(BaseHandler): RESPONDS_TO = EventType.Name(EventType.PATIENT_TIMELINE__GET_CONFIGURATION) def compute(self) -> list[Effect]: return [PatientTimelineEffect(allowed_new_note_types=[]).apply()] ``` ### Behavior What your plugin returns| Result ---|--- attribute omitted, or `None`| the full note type list, unchanged `allowed_new_note_types=[...]`| only those note types are offered `allowed_new_note_types=[]`| nothing is offered, so the **New Note** button is hidden entirely - **Multiple plugins** : allow-lists from all responses are combined, the same way exclusions are. Note the consequence: a second plugin returning an allow-list _widens_ what a first one permits, so a restriction is only as tight as the most permissive plugin responding. - **Combined with exclusions** : a note type excluded via `excluded_note_types` stays out of the button even if the allow-list names it. Exclusions win because they affect far more — the timeline, the note type filter and permalink access — so they are the safer outcome when a plugin names the same type in both. - **Chart Review** : unlike exclusions, `CHART_REVIEW` is _not_ force-allowed here. Force-allowing it would make "nothing available" unreachable and the button could never be hidden. - **Plugin failures** : if the plugin runner cannot be reached, the note type list is left unconstrained rather than emptied. > **Warning:** **This is a workflow guardrail, not an access control.** It governs what the **New Note** button offers. It does not reject a note of a restricted type created directly through the API. Do not rely on it to enforce access to sensitive note types — see [Note Restrictions](/sdk/effect-note-restrictions/) for controlling access to notes. > **Info:** **Note types are configured per instance.** The names above are illustrative, so check what exists on your instance before matching on `name` — a name that does not exist simply matches nothing, silently shortening your allow-list. A `unique_identifier` is generated per instance too, so it cannot be hard-coded in a plugin meant to run on more than one; look the note types up at runtime and keep the mapping configurable. An identifier that does not exist raises a `ValidationError` rather than failing quietly. --- # Patient Effect Source: https://docs.canvasmedical.com/sdk/effect-patient/ The `Patient` effect enables the creation and updating of patient records within the Canvas system. This effect captures demographic information, contact details, and clinical associations necessary for patient registration and updates. Build a `Patient` with the attributes you want to set, then return one of its two methods from your handler: `create()` for a new patient, or `update()` for an existing one. ## Methods ### create() → Effect Creates a new patient record. - `first_name` and `last_name` are required. - `patient_id` is optional. By default Canvas generates the patient's id; to choose it yourself, see Supplying a patient id on creation. See Creating a patient for an example. ### update() → Effect Updates an existing patient record. - `patient_id` is required, and must be the id of an existing patient. - Only the attributes you set on the effect are changed. Attributes you leave unset keep their current values. See Updating a patient for an example. Both methods check that `default_location_id` and `default_provider_id`, when set, refer to an existing practice location and staff member. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `first_name`| `str`| Patient's first name| For `create()` `last_name`| `str`| Patient's last name| For `create()` `middle_name`| `str` or `None`| Patient's middle name| No `birthdate`| `datetime.date` or `None`| Patient's date of birth| No `prefix`| `str` or `None`| Name prefix (e.g., "Dr.", "Mr.")| No `suffix`| `str` or `None`| Name suffix (e.g., "Jr.", "III")| No `sex_at_birth`| `PersonSex` or `None`| Patient's sex assigned at birth| No `nickname`| `str` or `None`| Patient's preferred name or nickname| No `social_security_number`| `str` or `None`| Patient's SSN| No `administrative_note`| `str` or `None`| Administrative notes about the patient| No `clinical_note`| `str` or `None`| Clinical notes about the patient| No `default_location_id`| `str` or `None`| The `id` of the [PracticeLocation](/sdk/data-practicelocation/#practicelocation) to set as the patient's default practice location| No `default_provider_id`| `str` or `None`| The `id` of the [Staff](/sdk/data-staff/#staff) member to set as the patient's default provider| No `active`| `bool` or `None`| Whether the patient record is active| No `deceased`| `bool` or `None`| Whether the patient is deceased| No `deceased_datetime`| `datetime.datetime` or `None`| Date and time of patient's death| No `deceased_cause`| `str` or `None`| Cause of patient's death| No `deceased_comment`| `str` or `None`| Additional comments about patient's death| No `biological_race_codes`| `list[str]` or `None`| CDC race codes describing the patient's biological race (e.g., `"2106-3"`)| No `cultural_ethnicity_codes`| `list[str]` or `None`| CDC ethnicity codes describing the patient's cultural ethnicity (e.g., `"2186-5"`)| No `previous_names`| `list[str]` or `None`| List of patient's previous names| No `contact_points`| list[PatientContactPoint] or `None`| Patient's contact information| No `contacts`| list[PatientContact] or `None`| The patient's contacts — emergency contacts, next-of-kin, and other related persons. See Managing patient contacts| No `external_identifiers`| list[PatientExternalIdentifier] or `None`| Patient's external identifiers| No `patient_id`| `str` or `None`| Patient id. Optional on creation, where it must be a 32-character hex string (a UUID4 without hyphens); see Supplying a patient id on creation.| For `update()` `addresses`| list[PatientAddress] or `None`| Patient's addresses| No `preferred_pharmacies`| list[PatientPreferredPharmacy] or `None`| Patient's preferred pharmacies| No `metadata`| list[PatientMetadata] or `None`| Patient metadata| No ## PatientContactPoint The `PatientContactPoint` dataclass represents various methods of contacting the patient. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `system`| `ContactPointSystem`| Type of contact (e.g., phone, email)| Yes `value`| `str`| The contact information value (e.g., phone number, email address)| Yes `use`| `ContactPointUse`| Purpose of the contact point (e.g., home, work)| Yes `rank`| `int`| Priority order of contact methods| Yes `has_consent`| `bool` or `None`| Whether consent has been given to use this contact method| No > **Info:** If you have already validated a patient's phone or email in a prior workflow, set `has_consent=True` on the `PatientContactPoint` when creating the patient. This marks the contact point as okay to text or email and suppresses the additional 'click here' verification message that Canvas would otherwise send. ## PatientContact The `PatientContact` dataclass represents one of the patient's contacts — an emergency contact, next-of-kin, or other related person. A contact identifies its person in one of two ways, and you must supply one of them: either **inline** , by giving a `name` (with optional phone, email and comments), or by **reference** , by pointing `related_patient` at another Canvas patient. The reference form is what links two patients to each other, and Canvas displays such a contact from the referenced patient's own record rather than from the contact row. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `name`| `str` or `None`| The contact's name, when the contact holds the person's details inline| One of `name` or `related_patient` `related_patient`| `str`, `uuid.UUID` or `None`| The patient id of an existing Canvas patient this contact refers to, used instead of `name`| One of `name` or `related_patient` `contact_identifier`| `str`, `uuid.UUID` or `None`| Identifies an existing contact. Omit it to add a contact; supply it to modify or remove one. See Managing patient contacts| No `phone_number`| `str` or `None`| The contact's phone number. Exactly 10 digits| No `email`| `str` or `None`| The contact's email address| No `comments`| `str` or `None`| Free-text notes about the contact| No `categories`| list[PatientContactCategory] or `None`| The contact's relationship categories| No `inactive`| `bool` or `None`| Set with `contact_identifier` to remove the contact| No ## PatientContactCategory The `PatientContactCategory` dataclass expresses a contact's relationship to the patient — emergency contact, next-of-kin, and so on — as a coding. All three fields are required, and the coding must already exist in the instance. Look one up with the [ContactCategory](/sdk/data-patient/#contactcategory) data model rather than composing a coding by hand; a coding the instance does not have raises a validation error instead of being created. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `code`| `str`| The category code (e.g., `"EMC"` for an emergency contact)| Yes `code_system`| `str`| The coding system the code belongs to (e.g., `"INTERNAL"`)| Yes `name`| `str`| The category's display name (e.g., `"Emergency contact"`)| Yes ## PatientExternalIdentifier The `PatientExternalIdentifier` dataclass represents an external identifier (ID) associated with the patient. An example would be the unique patient ID for a third party system integrated with Canvas EMR. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `system`| `str`| URL of the system of origin for the external ID (e.g., `http://hl7.org/fhir/sid/us-ssn`)| Yes `value`| `str`| The external ID or membership number/value| Yes ## PatientAddress The `PatientAddress` dataclass represents a patient's address information. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `line1`| `str`| Street address line 1| Yes `line2`| `str` or `None`| Street address line 2| No `city`| `str`| City name| Yes `state_code`| `str`| State code (e.g., "CA", "NY")| Yes `postal_code`| `str`| Postal/ZIP code| Yes `country`| `str`| Country code| Yes `use`| `AddressUse`| Address type (e.g., home, work)| Yes > **Warning:** Address updates are **replace-based**. When updating a patient's addresses, the provided address list will completely replace all existing addresses. If you provide an empty list, all existing addresses will be deleted. ## PatientPreferredPharmacy The `PatientPreferredPharmacy` dataclass represents a patient's preferred pharmacy, and if it's their default pharmacy. Attribute| Type| Description| Required ---|---|---|--- `ncpdp_id`| `str`| The ncpdp ID of the pharmacy| Yes `default`| `bool`| True if it's the default pharmacy| Yes ## PatientMetadata The `PatientMetadata` dataclass represents a custom key-value pair for a patient. Attribute| Type| Description| Required ---|---|---|--- `key`| `str`| The key of the metadata| Yes `value`| `str`| The value of the metadata| Yes ## Implementation Details - Structures contact information through the `PatientContactPoint` dataclass - Structures the patient's contacts through the `PatientContact` dataclass, added or modified per entry according to `contact_identifier` — see Managing patient contacts - Structures external identifier through the `PatientExternalIdentifier` dataclass - Structures address information through the `PatientAddress` dataclass - Structures metadata through the `PatientMetadata` dataclass ## Example Usage ### Creating a patient ```python from canvas_sdk.effects.patient import Patient, PatientContactPoint, PatientExternalIdentifier, PatientMetadata from canvas_sdk.handlers.base import BaseHandler from canvas_sdk.v1.data.common import ContactPointSystem, ContactPointUse, PersonSex import datetime class MyHandler(BaseHandler): def compute(self): patient = Patient( first_name="Jane", last_name="Doe", middle_name="Marie", birthdate=datetime.date(1980, 1, 15), sex_at_birth=PersonSex.SEX_FEMALE, nickname="Janie", default_location_id="location-uuid", default_provider_id="provider-uuid", contact_points=[ PatientContactPoint( system=ContactPointSystem.PHONE, value="555-123-4567", use=ContactPointUse.MOBILE, rank=1, has_consent=True ), PatientContactPoint( system=ContactPointSystem.EMAIL, value="jane.doe@example.com", use=ContactPointUse.WORK, rank=2, has_consent=True ) ], external_identifiers=[ PatientExternalIdentifier( system="http://www.aaa.com", value="pat_id_123456" ) ], metadata = [ PatientMetadata(key="source", value="plugin"), PatientMetadata(key="created_on", value=datetime.datetime.now().isoformat()) ] ) return [patient.create()] ``` ### Updating a patient ```python from canvas_sdk.effects.patient import Patient, PatientAddress, PatientExternalIdentifier from canvas_sdk.handlers.base import BaseHandler from canvas_sdk.v1.data.common import AddressUse class MyHandler(BaseHandler): def compute(self): # Update an existing patient updated_patient = Patient( patient_id="existing-patient-uuid", first_name="Jane", last_name="Smith", # Changed last name addresses=[ PatientAddress( line1="456 Updated Street", line2="Suite 200", city="Updated City", state_code="CA", postal_code="90210", country="US", use=AddressUse.HOME ) ], external_identifiers=[ PatientExternalIdentifier( system="http://www.updated-system.com", value="new_patient_id_789" ) ] ) return [updated_patient.update()] ``` ### Marking a Patient as Inactive or Deceased ```python from canvas_sdk.effects.patient import Patient from canvas_sdk.handlers.base import BaseHandler import datetime class MyHandler(BaseHandler): def compute(self): # Mark a patient as inactive inactive_patient = Patient( patient_id="existing-patient-uuid", active=False ) return [inactive_patient.update()] class DeceasedPatientHandler(BaseHandler): def compute(self): # Record a patient's death deceased_patient = Patient( patient_id="existing-patient-uuid", deceased=True, deceased_datetime=datetime.datetime(2025, 3, 14, 12, 0, 0), deceased_cause="Natural causes", deceased_comment="Pronounced at home." ) return [deceased_patient.update()] ``` ## Supplying a patient id on creation By default, Canvas generates the patient id (`patient_id`) when you create a patient. You can supply your own instead by passing a 32-character hex string (a UUID4 with its hyphens removed) in the `patient_id` parameter of `Patient`. This lets your plugin generate the id up front and reuse it for follow-up, patient-scoped effects — such as notes or commands — in the same plugin execution, without reading the id back first. It works the same way Notes and Commands accept a pre-generated id. A supplied id must be a well-formed patient id: a 32-character lowercase hex string, which is a UUID4 with its hyphens removed. Use `generate_patient_id()` to produce one rather than building the format by hand. An id in any other format — for example, a hyphenated or uppercase UUID — raises a validation error on `create()`, as does an id that already belongs to an existing patient. Since `generate_patient_id()` returns a fresh, well-formed id, it satisfies both requirements. If you omit `patient_id`, the server generates the id as before, so existing plugins are unaffected. Because you generate the id up front, you can also return it to the caller from a [SimpleAPI](/sdk/handlers-simple-api-http/) endpoint — so a client creating the patient gets the id back in the response instead of having to look it up afterward. This example authenticates with the [`APIKeyAuthMixin`](/sdk/handlers-simple-api-http/), which expects a `simpleapi-api-key` secret declared in your manifest: ```python from canvas_sdk.effects.patient import Patient, generate_patient_id from canvas_sdk.effects.simple_api import JSONResponse, Response from canvas_sdk.handlers.simple_api import APIKeyAuthMixin, SimpleAPIRoute class CreatePatientAPI(APIKeyAuthMixin, SimpleAPIRoute): PATH = "/patients" def post(self) -> list[Response]: body = self.request.json() new_patient_id = generate_patient_id() patient = Patient( patient_id=new_patient_id, first_name=body["first_name"], last_name=body["last_name"], ) # `new_patient_id` can be reused for follow-up patient-scoped effects in # the same execution, and is returned so the caller has it immediately # without a follow-up lookup. return [ patient.create(), JSONResponse({"patient_id": new_patient_id}, status_code=201), ] ``` ## Managing patient contacts The `contacts` field writes the patient's contacts — emergency contacts, next-of-kin, and other related persons. What happens to each entry is decided by **`contact_identifier`** , not by whether you called `create()` or `update()`: `contact_identifier`| `inactive`| Result ---|---|--- omitted| omitted| The contact is **added** supplied| omitted| The contact it names is **modified** supplied| `True`| The contact it names is **removed** omitted| `True`| Validation error — there is no contact to remove So `Patient(...).update()` adds a contact to a patient that already exists, which is the usual case for a plugin populating contacts after intake. Re-sending an identical contact matches the existing one rather than adding a second, so a handler that re-emits the same contact on every event will not accumulate duplicates. On an update, a `contact_identifier` that names no contact on that patient is treated as a mistake and raises rather than being added. Contacts you leave out of the list are **left alone**. Unlike `addresses`, this field is not replace-based: omitting a contact never deletes it, and removal is always explicit through `inactive`. An update writes only the fields you send, so changing a phone number does not blank the email or the comments. `name` (or `related_patient`) is the exception — every contact that is not a removal needs one, so resend the existing value when you are changing something else. Pass an empty string to clear a stored value deliberately. ```python from canvas_sdk.effects.patient import Patient, PatientContact, PatientContactCategory from canvas_sdk.handlers.base import BaseHandler from canvas_sdk.v1.data import ContactCategory class MyHandler(BaseHandler): def compute(self): # Look the coding up rather than composing one — an unknown coding raises. emergency = ContactCategory.objects.get(code="EMC") category = PatientContactCategory( code=emergency.code, code_system=emergency.system, name=emergency.name, ) # No contact_identifier, so this adds a contact. patient = Patient( patient_id="existing-patient-id", contacts=[ PatientContact( name="Jane Doe", phone_number="5551234567", email="jane@example.com", comments="Primary emergency contact", categories=[category], ) ], ) return [patient.update()] ``` ### Linking one patient to another Setting `related_patient` to another patient's key makes that patient the contact. Because your plugin can supply the patient id on creation, it knows the key before the patient exists — so it can create a patient and reference it from a later effect in the same execution: ```python from canvas_sdk.effects.patient import Patient, PatientContact, generate_patient_id from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): spouse_key = generate_patient_id() spouse = Patient( patient_id=spouse_key, first_name="Alex", last_name="Doe", ) # References a patient the previous effect creates. Effects are applied in # order, so the key resolves by the time this one is written. patient = Patient( patient_id="existing-patient-id", contacts=[ PatientContact( related_patient=spouse_key, comments="Spouse — also a patient in Canvas", ) ], ) return [spouse.create(), patient.update()] ``` A `related_patient` contact carries no name of its own; Canvas shows the referenced patient's details instead. ### Removing a contact A removal needs the `contact_identifier` of the contact to remove and nothing else — no name or related patient, since neither is meaningful on a delete. Read the identifier from the [PatientContactPerson](/sdk/data-patient/#patientcontactperson) data model: ```python from canvas_sdk.effects.patient import Patient, PatientContact from canvas_sdk.handlers.base import BaseHandler from canvas_sdk.v1.data import PatientContactPerson class MyHandler(BaseHandler): def compute(self): patient_key = "existing-patient-key" contact = PatientContactPerson.objects.filter( patient__id=patient_key, name="Jane Doe" ).first() if contact is None: return [] patient = Patient( patient_id=patient_key, contacts=[ PatientContact(contact_identifier=str(contact.id), inactive=True) ], ) return [patient.update()] ``` A single `contacts` list may mix all of these — additions, modifications and removals travel together in one effect. ## Setting Race and Ethnicity `biological_race_codes` and `cultural_ethnicity_codes` each accept a list of code strings drawn from the [CDC Race and Ethnicity CodeSystem (CDCREC)](https://hl7.org/fhir/us/core/STU3.1.1/CodeSystem-cdcrec.html) — the same code set used by the [FHIR Patient API](/api/patient/). You can set both fields when creating or updating a patient, and you can supply more than one code per field. Canvas recognizes the full CDCREC code set — both the OMB top-level categories below and the more specific detailed codes that roll up to them (for example, the race code `2108-9` "European" rolls up to `2106-3` "White", and the ethnicity code `2148-5` "Mexican" rolls up to `2135-2` "Hispanic or Latino"). The categories below are the most common values; see the CodeSystem for the complete list of detailed codes. **Race** (`biological_race_codes`) — OMB top-level categories: Code| Description ---|--- `1002-5`| American Indian or Alaska Native `2028-9`| Asian `2054-5`| Black or African American `2076-8`| Native Hawaiian or Other Pacific Islander `2106-3`| White `2131-1`| Other Race **Ethnicity** (`cultural_ethnicity_codes`) — OMB top-level categories: Code| Description ---|--- `2135-2`| Hispanic or Latino `2186-5`| Not Hispanic or Latino ```python from canvas_sdk.effects.patient import Patient from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): patient = Patient( patient_id="existing-patient-uuid", biological_race_codes=["2106-3"], # White cultural_ethnicity_codes=["2186-5"] # Not Hispanic or Latino ) return [patient.update()] ``` ## Validation The effect performs validation before execution to ensure data integrity: 1. **Required Fields** : - For creation: Validates that mandatory fields like `first_name` and `last_name` are provided - For updates: Requires `patient_id` to be provided and verifies the patient exists in the database 2. **Referenced Entity Validation** : Confirms that any referenced entities exist in the system: - Verifies that the specified default practice location exists - Ensures that the specified default provider exists 3. **Data Format Validation** : Ensures that provided values conform to expected formats: - Date fields must be valid dates - Enumerated types like `PersonSex`, `ContactPointSystem`, and `ContactPointUse` must contain valid values - On creation, if `patient_id` is supplied it must be a well-formed patient id (a 32-character hex string); otherwise validation raises - On creation, a supplied `patient_id` must not already belong to an existing patient; a duplicate id raises a validation error 4. **Update-Specific Validation** : - Validates that the patient exists before attempting updates 5. **Contact Validation** (see Managing patient contacts): - Every contact that is not a removal must carry either `name` or `related_patient` - A removal (`inactive=True`) must carry `contact_identifier` - `contact_identifier` and `related_patient` must be UUIDs; on an update, `contact_identifier` must name a contact that belongs to this patient, and `related_patient` must name an existing patient - `phone_number` must be exactly 10 digits, and `email` must be a valid email address - `PatientContactCategory` requires `code`, `code_system` and `name`, and the coding must already exist in the instance — an unknown coding raises rather than being created --- # Phone Dial Configuration Effect Source: https://docs.canvasmedical.com/sdk/effect-phone-dial-configuration/ The `PhoneDialConfiguration` effect turns the phone numbers in a patient chart into clickable links — the click-to-dial affordance. It lets a user click to dial a number, or hand it to a softphone, directly from the chart. Return it in response to the [`PHONE_DIAL__GET_CONFIGURATION`](/sdk/events/#phone-dial-configuration) event, which Canvas fires as a chart loads its phone numbers. Import the effect and its enums from the submodule: ```python from canvas_sdk.effects.phone_dial_configuration import ( PhoneDialClickHandling, PhoneDialConfiguration, PhoneDialSection, ) ``` Build a `PhoneDialConfiguration` with the attributes you want to set, then return its `apply()` from your handler. * * * ## Methods ### apply() → Effect Makes the listed sections' numbers clickable. - `clickable_sections` is required, with at least one section. ## How it works As a patient chart loads phone numbers, Canvas fires `PHONE_DIAL__GET_CONFIGURATION`. A handler subscribed to the event returns one or more `PhoneDialConfiguration` effects, each naming the chart sections it makes clickable and how a click on those sections is handled. If no plugin returns a configuration, every number renders as plain text, except the patient header's primary number when your instance has Dialing Service settings configured. See Dialing Service settings below. A single effect carries one `click_handling` mode that applies to every section it lists. To handle sections differently — some dialed by the device, some by your plugin — return one effect per handling mode. A click is either dialed by the device or handed to your plugin, depending on the `click_handling` mode the effect carries. Two categories of numbers are never affected by this configuration: sections you do not list render as plain text, and fax numbers are always plain text regardless of configuration. When more than one plugin responds, Canvas merges their configurations: - A section is clickable if any plugin lists it. - A section is plugin-driven if any plugin asks for `PLUGIN` on it. - Among the remaining configurations, the first one that names a `dial_label` supplies the label. ## Dialing Service settings Click-to-dial also exists as an instance setting, independent of any plugin. When both `DIALING_LABEL` and `DIALING_URL_TEMPLATE` are set under Dialing Service, the patient header's primary number becomes clickable on its own, dialed by the device and labeled with `DIALING_LABEL`. Those settings reach only the patient header, so no other section is affected by them. > **Warning:** A plugin that answers `PHONE_DIAL__GET_CONFIGURATION` replaces the Dialing Service settings rather than adding to them. A responding plugin speaks for every section, including the ones it leaves out, so the patient header stops using `DIALING_URL_TEMPLATE` even if your configuration never mentions the patient section. To keep that behavior, list the patient section in your own configuration. ## Event payload Property| Value| Description ---|---|--- `event.target.id`| `str` (UUID)| The id of the [Patient](/sdk/data-patient/#patient) whose chart is loading phone numbers. `event.actor`| user| The [logged-in user](/sdk/events/#event-actor) viewing the chart, when available. `event.context`| `{}`| Empty — no additional context is provided. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `clickable_sections`| list[PhoneDialSection]| The chart sections whose numbers become clickable. Provide at least one — an empty list is rejected.| Yes `click_handling`| PhoneDialClickHandling| How a click on the listed sections is handled. Defaults to `DEVICE`.| No `dial_label`| `str` | `None`| A display affordance only. When `dial_label` is set, the section shows the number plus a "Dial number with `