# 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/ ## Overview This allows developers to dynamically display additional fields when scheduling an appointment. ```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="" ), ]) ``` ## Structure ### **FormField** A FormField consists of the following properties: #### Attributes Attribute| Type| Description ---|---|--- `key`| `str`| unique identifier of the field - appointment metadata key `label`| `str`| the label that will be displayed on the field `type`| `InputType`| the type of the input - TEXT, SELECT, DATE. `required`| `bool`| if the input is required. `editable`| `bool`| if the input can be editabled. `options`| `list[str]`| possible options for when the input type is set to "SELECT" `value`| `str`| default value for the field ### **AppointmentsMetadataCreateFormEffect** An AppointmentsMetadataCreateFormEffect consists of the following properties: #### Attributes Attribute| Type| Description ---|---|--- `form_fields`| `list[FormField]`| list of fields. --- # 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. * * * ## 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/ ## Overview 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. ```python from canvas_sdk.effects.calendar import Calendar, CalendarType Calendar( provider="provider-uuid", type=CalendarType.Clinic, location="location-uuid", description="Primary clinic calendar" ).create() ``` ## Structure ### **CalendarType** An enumeration of calendar types: Value| Description ---|--- `Clinic`| Calendar for clinical appointments `Administrative`| Calendar for administrative tasks ### **Calendar** A Calendar effect consists of the following properties: #### Attributes Attribute| Type| Description ---|---|--- `id`| `str \| UUID \| None`| Optional unique identifier for the calendar. `provider`| `str \| UUID`| The provider UUID `type`| `CalendarType`| The type of calendar - either `CalendarType.Clinic` or `CalendarType.Administrative` `location`| `str \| UUID \| None`| location UUID to associate with the calendar. `description`| `str \| None`| description of the calendar's purpose. --- # Calendar Event Management Source: https://docs.canvasmedical.com/sdk/calendar-event-management-effects/ ## Overview 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. ```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() ``` ## Structure ### **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 ### **Event** An Event effect consists of the following properties: #### Attributes Attribute| Type| Description ---|---|--- `calendar_id`| `str \| UUID \| None`| The calendar UUID where the event will be created. `event_id`| `str \| UUID \| None`| The event UUID to update. `title`| `str \| None`| The title of the event. `starts_at`| `datetime \| None`| The start date and time of the event. `ends_at`| `datetime \| None`| The end date and time of the event. `recurrence_frequency`| `EventRecurrence \| None`| The frequency of recurrence - either `EventRecurrence.Daily` or `EventRecurrence.Weekly`. `recurrence_interval`| `int \| None`| The interval between recurrences (e.g., 1 for every week, 2 for every other week). `recurrence_days`| `list[DaysOfWeek] \| None`| List of days when the event should recur (used with weekly recurrence). `recurrence_ends_at`| `datetime \| None`| The date and time when the recurrence pattern ends. `allowed_note_types`| `list[str] \| None`| List of note types that are allowed for this event. --- # Canvas CLI Source: https://docs.canvasmedical.com/sdk/canvas_cli/ ## 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`. ### Configuration and Authenticating to Your Canvas Instance Create a file `~/.canvas/credentials.ini` with sections for each of your Canvas instance subdomains, and add client_id and client_secret credentials to each section. For example, if your Canvas instance url is `https://buttered-popcorn.canvasmedical.com/`, you would have a section `[buttered-popcorn]` with key-value pairs for `client_id` and `client_secret`. > **Info:** **Getting Credentials:** Learn how to get register a client_id and client_secret [here](/api/customer-authentication/#registering-a-third-party-application-on-canvas). > The Canvas CLI uses OAuth, just like the FHIR API. **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. **You are now ready to use the Canvas CLI** ## 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`. ## Usage ```console $ canvas [OPTIONS] COMMAND [ARGS]... ``` **Options** : - `--version` - `--help`: Show this message and exit. ## Commands - `init`: Create a new plugin - `install`: Install a plugin into a Canvas instance - `uninstall`: Uninstall a plugin from a Canvas instance - `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 on a Canvas instance ### `canvas init` Create a new plugin. **Usage** : ```console $ canvas init [OPTIONS] ``` **Options** : - `--help`: Show this message and exit. ### `canvas install` 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** : Before uploading, `canvas install` runs the same pre-flight validation as `canvas validate`: - 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 any handler fails to load — for example, due to a disallowed import like `subprocess` — the install aborts before the plugin is built or uploaded, so it never reaches your instance. Run `canvas validate` first for detailed per-handler results. The CLI automatically excludes common build artifacts from the plugin bundle: - `__pycache__` directories - `*.pyc` and `*.pyo` files - `node_modules` directories - Hidden files and directories (e.g., `.git`, `.env`) To exclude additional files, create a `.canvasignore` file in your plugin directory. This file follows the same syntax as [.gitignore](https://git-scm.com/docs/gitignore). Example ```md # Exclude test files test_*.py ``` ### `canvas uninstall` Uninstall a plugin from a Canvas instance. **Usage** : ```console $ canvas uninstall [OPTIONS] NAME ``` **Arguments** : - `NAME`: Plugin name to delete [required] **Options** : - `--force`: Force uninstallation of the plugin - `--host TEXT`: Canvas instance to connect to - `--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 [setattr-blocked] `setattr()` is blocked by the sandbox. Use direct attribute assignment (`obj.attr = value`) instead. ``` **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 ---|--- `setattr-blocked`| `setattr(obj, "x", value)` — use `obj.x = value` `delattr-blocked`| `delattr(obj, "x")` — use `del obj.x` `bytearray-blocked`| `bytearray(...)` — use `bytes` for binary data `type-blocked`| Any call to `type()`. It is absent from the sandbox builtins, so even the one-argument `type(x)` raises `NameError` — use `isinstance(x, SomeClass)` or `x.__class__.__name__`, and declare classes with `class …:` rather than `type(name, bases, dict)` `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. 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. #### 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:** `canvas install` runs this same static lint and sandbox-load validation before uploading, so violations are caught before they reach your 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:** `canvas install` runs manifest validation, the static lint, and sandbox-load validation before uploading. Use `canvas validate` for a full pre-flight check 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 on a Canvas instance. Each variable must already be declared in the plugin's `CANVAS_MANIFEST.json`. Pass one or more `KEY=value` pairs as positional arguments. **Usage** : ```console $ canvas config set [OPTIONS] PLUGIN VARIABLES... ``` **Examples** : Set a single variable: ```console $ canvas config set my_plugin API_TOKEN=your_api_token_value ``` Set multiple variables in one call: ```console $ canvas config set my_plugin API_TOKEN=abc123 LOG_LEVEL=info ``` 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 my_plugin $'REDIRECT_ALLOWLIST_INTERNAL=/panel\n/patient' ``` **Arguments** : - `PLUGIN`: Plugin name to set variables for - `VARIABLES...`: Variables to set, e.g. Key=value **Options** : - `--host TEXT`: Canvas instance to connect to - `--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. --- # 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. ## 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/ ## Overview 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`. The effect is returned from a handler that responds to the `COMMAND__FORM__GET_ADDITIONAL_FIELDS` event. ```python from canvas_sdk.effects.command_metadata import ( CommandMetadataCreateFormEffect, FormField, InputType, ) CommandMetadataCreateFormEffect( command_uuid="command-uuid", form_fields=[ FormField( key="reason", label="Reason", type=InputType.SELECT, options=["Routine", "Follow-up", "Other"], ), ], ) ``` ## Structure ### **FormField** A FormField consists of the following properties: #### Attributes Attribute| Type| Description ---|---|--- `key`| `str`| unique identifier of the field - command metadata key `label`| `str`| the label that will be displayed on the field `type`| `InputType`| the type of the input - TEXT, SELECT, DATE. `required`| `bool`| if the input is required. `editable`| `bool`| if the input can be editabled. `options`| `list[str]`| possible options for when the input type is set to "SELECT" `value`| `str`| default value used only when no CommandMetadata row exists for this key. If the user has previously saved a value (including a cleared/empty value) the stored row wins and this field is ignored. ### **CommandMetadataCreateFormEffect** A CommandMetadataCreateFormEffect consists of the following properties: #### Attributes Attribute| Type| Description ---|---|--- `command_uuid`| `str`| the UUID of the command these fields should be rendered on. `form_fields`| `list[FormField]`| list of fields. ## Validation The effect validates inputs before it is applied: - `command_uuid` is 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. ## Example Usage 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` 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. ## Limitations - Custom commands are read-only and cannot capture user input - Interactive elements (forms, buttons, input fields) are not supported - 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. ## 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. ### 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` belongs to [custom commands](/sdk/commands-custom-command/) alone. 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 DiagnoseCommand def compute(): diagnose_command = DiagnoseCommand( note_uuid='550e8400-e29b-41d4-a716-446655440000', icd10_code='E11.9' ) return [diagnose_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()] ``` #### 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. **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 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 DiagnoseCommand def compute(): diagnose_command = DiagnoseCommand( note_uuid='550e8400-e29b-41d4-a716-446655440000', icd10_code='E11.9' ) return [diagnose_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 is also required for questionnaire-based commands, where `originate()` creates the command but does not add the answers — you must chain an `edit()` to populate the responses (see Usage Example). This chaining is necessary because the `originate` method executes asynchronously, so there is no way to get the `command_uuid` back from the originate action and use it for subsequent actions in the same operation. ```python from uuid import uuid4 from canvas_sdk.commands import DiagnoseCommand def compute(): note_uuid = '550e8400-e29b-41d4-a716-446655440000' diagnose_command = DiagnoseCommand( 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. diagnose_command.command_uuid = str(uuid4()) # Now we can both originate and commit in a single operation return [diagnose_command.originate(), diagnose_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. > **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. ## Commands The sections below document each command class. See Common Attributes for the parameters and methods shared by all commands. ### Custom Commands For creating custom commands with HTML-rendered content that can be inserted into patient charts, see the [CustomCommand](/sdk/commands-custom-command/) documentation. Custom commands are different from standard commands: - They allow you to display read-only HTML content in the patient chart - They must be configured in your plugin's manifest before use - They support both display and print versions of content - They are designed for displaying formatted data, not for capturing user input Learn more: [CustomCommand Reference](/sdk/commands-custom-command/) * * * ### AdjustPrescription **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 **Command-specific parameters** : Name| Type| Required to commit| Description ---|---|---|--- `condition_id`| _string_| `true`| The id of the [Condition](/sdk/data-condition/#condition) being assessed. Must be a condition already recorded on that patient's chart. `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. **Example** : ```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** : `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. The check needs that note or command to exist, so it is skipped when you create the note and originate the command in the same batch of effects. Nothing is rejected in that case, since there is not yet a chart to compare the condition against. * * * ### ChangeMedication **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. **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. **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 **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. **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. `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. **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. *`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. **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. 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). **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 **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 **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` **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' ) ) ``` * * * ### SurgicalHistory **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. **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. **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 **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. **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 [Conditions](/sdk/data-condition/#condition) on the patient's active 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. *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. **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. `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. 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 import uuid 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), command_uuid=str(uuid.uuid4()), 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"), ], ), ], ) # Because we're directly setting a command_uuid, we can return both originate and edit. return [command.originate(), command.edit()] ``` 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 import uuid 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) command.command_uuid = str(uuid.uuid4()) # 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") # Because we're directly setting a command_uuid, we can return both originate and edit. return [command.originate(), command.edit()] ``` #### 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:** When creating a new questionnaire command, you must explicitly set a unique `command_uuid`. Providing this UUID enables you to originate the command within the note and then subsequently edit it with detailed responses in the same protocol execution. - This approach is necessary because given the dynamic nature of the questionnaire command, the initial creation (origination) only includes the questionnaire ID. Once the command has been originated, you can immediately follow up with an edit to populate it with the patient's responses. - If you are looking to insert a committed questionnaire command, you'll need to return three effects: - An `.originate()` to insert the command and select the questionnaire - An `.edit()` to populate the responses - A `.commit()` to commit the command * * * ### 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. **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()`. > **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 **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." ) ``` * * * ### Resolve Condition **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 = '' 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. `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 **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. **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. `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. **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 **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 a [Condition](/sdk/data-condition/#condition) already on that patient's chart. `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. `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' ) ``` * * * ### 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). **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 ) ``` ### ServiceProvider `ServiceProvider` represents detailed information about healthcare service providers, used in referral and imaging order 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`. 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. ## 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 }) ``` 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. Reinstall the plugin: `canvas install my_plugin --host 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. ### 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. 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` with a namespace and access level. The namespace is a unique identifier scoped to your organization (formatted as `organization__name` with a double underscore), and the access level controls whether the plugin can read only or read and write data. When the first `read_write` plugin is installed into a namespace, the system automatically initializes a data namespace, prepares tables, and generates `namespace_read_access_key` and `namespace_read_write_access_key` secrets that control access for other plugins joining the same namespace. ```json { "sdk_version": "0.1.4", "plugin_version": "1.0.0", "name": "my_plugin", "variables": [ {"name": "namespace_read_write_access_key", "sensitive": false} ], "custom_data": { "namespace": "acme_corp__shared_data", "access": "read_write" } } ``` ### Step by Step 1. `canvas init` 2. When prompted for a name, enter `Hello Custom Data` 3. `cd hello-custom-data/hello_custom_data` 4. Open `CANVAS_MANIFEST.json` in your preferred editor. 5. Create a `custom_data` block: ```json "custom_data": { "namespace": "my_org__hello_custom_data", "access": "read_write" } ``` 6. Add `namespace_read_write_access_key` to the `secrets` array (the key will be generated by the system for you) 7. Next, create a `models` directory under the root of your plugin hierarchy, sibling to `CANVAS_MANIFEST.json` and `handlers` 8. Create an `__init__.py` file inside of `models` and open it in your editor. 9. Declare the following classes within the `__init__.py` file: ```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() ``` 10. Open `handlers/event_handlers.py` in your editor. Update the imports: ```python from hello_custom_data.models import CustomNote, NoteTag ``` 11. In the code, replace uses of `Note` with `CustomNote`. These objects behave the same as the SDK model. (See [Extending SDK Models](/sdk/custom-data-extending-sdk-models/) for why proxy models are used.) 12. 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}") ``` 13. Install the plugin to your development environment 14. Tail the logs with `canvas logs` 15. Log into Canvas, navigate to a patient chart, and create a new note In the logs you will see our message: `Note tagged by: hello-custom-data` What just happened? When you installed the plugin, a new database namespace called `my_org__hello_custom_data` was created. Within the namespace are tables that hold information owned by, and managed by, the `my_org` plugins. The `NoteTag` model you defined turned into a PostgreSQL table with the following structure: ```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/). ### 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 (recommended):** 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": "my_plugin", "variables": [ {"name": "namespace_read_write_access_key", "sensitive": false} ], "custom_data": { "namespace": "acme_corp__shared_data", "access": "read_write" } } ``` **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. - Bulk operations (e.g., `bulk_create`) are limited to 10,000 records at a time. ## 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 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 --- # 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() ``` ## 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 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 ### 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)[]| ## 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 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'] ``` ## 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 name| String description| String area_code| String subdomain| String active| Boolean state| BusinessLineState organization| [Organization](/sdk/data-organization) 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. ## 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 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 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 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 ---|--- 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 For external (non-staff) members, `staff` is empty and `organizational_entity` links to the external provider. #### Properties Name| Type| Description ---|---|--- service_provider| [ServiceProvider](/sdk/data-serviceprovider/#service-provider) | `None`| The external provider for this membership, resolved through its `organizational_entity`; `None` for internal members. ## 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() ``` ## 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) 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 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}") ``` ## 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 ---|--- 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)[] **Computed Properties** : - `total_charges`: Total charges for active line items - `total_paid`: Sum of paid amounts from postings - `total_adjusted`: Sum of adjustments and transfers - `balance`: Remaining balance (coverage + patient) - `total_patient_paid`: Paid amount by the patient - `total_payer_paid`: Paid amount 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 ### 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 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 --- # Command Source: https://docs.canvasmedical.com/sdk/data-command/ ## Introduction The `Command` model represents a [command](/sdk/commands/) in a note. ## 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 ---|--- 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) 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) 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.). ## 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 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 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 assessments| [Assessment](/sdk/data-assessment/#assessment)[] resolutions| [ResolveConditionEvent](/sdk/data-resolve-condition-event/#resolveconditionevent)[] ### ConditionCoding Field Name| Type ---|--- dbid| Integer 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. Coverages can also be linked to a [`Snapshot`](/sdk/data-snapshot/#snapshot), which provides access to insurance card images captured via the Canvas iOS application or uploaded through the coverages modal. ## 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| Integer 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 ---|--- dbid| Integer 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 ### 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 --- # 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| String severity| String reference| String issue_identifier| String issue_identifier_system| String detail| String evidence| DetectedIssueEvidence[] ### DetectedIssueEvidence Field Name| Type ---|--- id| UUID dbid| Integer system| String version| String code| String display| String user_selected| Boolean detected_issue| [DetectedIssue](/sdk/data-detected-issue/#detectedissue) --- # 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) --- # DocumentReference Source: https://docs.canvasmedical.com/sdk/data-document-reference/ # DocumentReference 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 ---|--- 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 (property) — the SDK object the document is attached to, or `None` document_url| String (property) — presigned S3 URL or absolute URL ### DocumentReferenceCoding A coding entry representing the type of a document reference. Field Name| Type ---|--- dbid| Integer 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 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) 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 ---|--- 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 (property) ### 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 system| String version| String code| String display| String user_selected| Boolean family_history| FamilyHistory --- # 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) 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) 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) 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 --- # 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 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 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 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 ``` ## 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) 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 task_ids| String results| ImagingReport[] ### 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 ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime review_mode| [DocumentReviewMode](/sdk/data-enumeration-types/#documentreviewmode) junked| Boolean requires_signature| Boolean 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 (property) — presigned S3 URL, or `None` if the report has no file codings| ImagingReportCoding[] ### ImagingReportCoding Field Name| Type ---|--- dbid| Integer 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) 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 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 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) narrative| String codings| InstructionCoding[] ### InstructionCoding Field Name| Type ---|--- dbid| Integer 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() ``` ## 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 ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime status| IntegrationTaskStatus type| String title| String channel| IntegrationTaskChannel patient| [Patient](/sdk/data-patient/#patient) service_provider| [ServiceProvider](/sdk/data-serviceprovider/#service-provider) reviews| IntegrationTaskReview[] #### Properties Property| Type| Description ---|---|--- is_fax| Boolean| Whether this is a fax task is_pending| Boolean| Whether this task is pending review is_processed| Boolean| Whether this task has been processed has_error| Boolean| Whether this task has an error is_junked| Boolean| Whether this task is junked ### IntegrationTaskReview Field Name| Type ---|--- 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 #### Properties Property| Type| Description ---|---|--- is_active| Boolean| 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 `Invoice` holds the statement's amounts and delivery details, not the rendered file. Canvas attaches the PDF to a [DocumentReference](/sdk/data-document-reference/#the-related-object), which you reach by resolving the [ContentType](/sdk/data-content-type/) for the invoice and matching `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 originator| [CanvasUser](/sdk/data-canvasuser/) recipient| [Patient](/sdk/data-patient/#patient) recipient_type| InvoiceRecipients total_amount| Decimal 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. 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. 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 name| String code| String code_system| String search_keywords| String active| Boolean custom| Boolean poc| Boolean fields| LabReportTemplateField[] ### LabReportTemplateField Field Name| Type ---|--- dbid| Integer 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 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") ``` ### Filtering Lab Results by Abnormal Values 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() ``` ## 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 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) 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[] ### 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 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/). ## 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| The isSuccess status of the received by fax delivered_by_fax| Boolean| The isSuccess status of the delivered by fax 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 created the letter (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 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) 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 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() ``` ## 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) ``` ## Attributes ### Medication Field Name| Type ---|--- id| UUID dbid| Integer 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)[] ### MedicationCoding Field Name| Type ---|--- dbid| Integer 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. `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. `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 (property)| 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`| `Boolean`| Whether delivery was successful. `failed`| `Boolean`| Whether delivery 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": { "id": 1095, "command_uuid": "691123c4-6c7d-415b-880b-2beefab9f64a" } } ``` #### 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")`. 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) ``` The `command_uuid` in a command object corresponds to the `id` field of the [Command](/sdk/data-command/) model, allowing you to retrieve the full command data: ```python from canvas_sdk.v1.data.note import Note from canvas_sdk.v1.data.command import Command note = Note.objects.get(id="89992c23-c298-4118-864a-26cb3e1ae822") # Find all command references in the note body for item in note.body: if item.get("type") == "command": command_uuid = item["data"]["command_uuid"] command = Command.objects.get(id=command_uuid) print(f"Command type: {command.schema_key}") print(f"Command data: {command.data}") ``` ### 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 note Locking a note captures it as a PDF showing the note at the moment of the lock. 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, so you get there through the note's state history rather than from the note itself. 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 lock event's `dbid`: ```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") lock_events = note.state_history.filter(state=NoteStates.LOCKED) 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 lock_events], status=DocumentReferenceStatus.CURRENT, ).first() url = document.document_url if document else None ``` > **Info:** A note can be locked more than once. Each lock 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. Only encounter, inpatient, and review note types are captured this way; other note types have no PDF. ### 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 To get the note type for a specific note, use the `note_type_version` attribute which provides access to 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 include a `data` field with `id` and `command_uuid`. See Querying on the body. 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 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 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 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 removed_allergies| QuerySet[[RemoveAllergyEvent](/sdk/data-remove-allergy-event/#removeallergyevent)]| All allergies 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 ### 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) ``` ## 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 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 system| String version| String code| String display| String user_selected| Boolean observation_component| ObservationComponent ### ObservationValueCoding Field Name| Type ---|--- dbid| Integer 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 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 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 ---|--- id| UUID dbid| Integer content_type| [ContentType](/sdk/data-content-type/#contenttype) object_id| Integer name| String active| Boolean type| OrganizationalEntityType ## Properties Name| Type| Description ---|---|--- service_provider| [ServiceProvider](/sdk/data-serviceprovider/#service-provider) | `None`| 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/ # PatientAdministrativeDocument 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 ``` ## 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 ---|--- 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) 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 (property) — 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 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 ---|--- id| UUID dbid| Integer 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/) (property) — the current signed document, or `None` document_references| QuerySet[[DocumentReference](/sdk/data-document-reference/)] (property) — references for the signed documents ### PatientConsentCoding Field Name| Type ---|--- dbid| Integer 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 (property) — presigned S3 URL patient_consent| QuerySet[PatientConsent] ### PatientConsentRejectionCoding Field Name| Type ---|--- dbid| Integer system| String version| String code| String display| String user_selected| Boolean patient_consents| QuerySet[PatientConsent] ## Enumeration types ### PatientConsentStatus Value| Label ---|--- accepted| Accepted accepted_via_patient_portal| Accepted Via Patient Portal rejected| Rejected 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") ``` ## 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 the original `url` or `title`), 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) ``` ## 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() ``` ## Attributes ### Patient Field Name| Type ---|--- 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)[] 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)[] conditions| [Condition](/sdk/data-condition/#condition)[] coverages| [Coverage](/sdk/data-coverage/#coverage)[] dependent_coverages| [Coverage](/sdk/data-coverage/#coverage)[] detected_issues| [DetectedIssue](/sdk/data-detected-issue/#detectedissue)[] devices| [Device](/sdk/data-device/#device)[] 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)[] 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)[] 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)[] ### PatientAddress Field Name| Type ---|--- id| UUID dbid| Integer 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 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 ```python from canvas_sdk.v1.data.patient import Patient from logger import log patient_id = "d7af3e356368446c85b40a5d6ff7288e" patient = Patient.objects.get(id=patient_id) patient_contacts = patient.telecom.all() 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 ### 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| String 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}") ``` ### PatientIdentificationCard Represents a patient identification card image (e.g., driver's license, insurance card). Field Name| Type ---|--- dbid| Integer created| DateTime modified| DateTime patient| Patient image| String title| String active| Boolean image_url| String (property) — 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 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)| "" ## Computed Properties ### Patient - `full_name`: The full name of the patient, combining first, middle, and last names. - `preferred_pharmacy`: The patient's preferred pharmacy for medication fulfillment. - `preferred_full_name`: The patient's preferred full name, if different from the legal name. - `preferred_first_name`: The patient's preferred first name, if different from the legal first name. - `primary_phone_number`: The patient's primary contact number. - `photo`: The patient's first uploaded avatar PatientPhoto, if any. - `photo_url`: A presigned URL for the patient's avatar photo, or the default avatar URL when no photo is set. --- # 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 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) 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`. 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 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() ``` ## Attributes ### BasePosting Base model for aggregating multiple line item-level transactions (payments, adjustments, transfers) associated with a claim. Field Name| Type ---|--- 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] **Computed Properties** : - `paid_amount`: Total paid - `contractual_adjusted_amount`: Adjustments marked as write-offs - `non_write_off_adjusted_amount`: Non-write-off adjustments - `transferred_amount`: Total transferred - `transferred_to_patient_amount`: Portion transferred to patient - `transferred_to_coverage_amount`: Portion transferred to another coverage - `adjusted_and_transferred_amount`: Combined adjusted and transferred amount - `posted_amount`: 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 ---|--- claim_patient| [ClaimPatient](/sdk/data-claim/#claimpatient) patient_payment| BulkPatientPosting copay| BulkPatientPosting **Computed Properties** : - `discounted_amount`: Discount applied - `charges_amount`: Discount + paid amount ### BulkPatientPosting Aggregates bulk patient payments on multiple claims. Field Name| Type ---|--- 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] **Computed Properties** : - `total_posted_amount`: Sum of all posted amounts - `discounted_amount`: Sum of discounted amounts ### BaseRemittanceAdvice Represents shared data for both electronic and manual remittance advice. Field Name| Type ---|--- 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] **Computed Properties** : - `total_posted_amount`: 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] ### NewLineItemPayment Represents a payment applied to a billing line item within a claim. Field Name| Type ---|--- dbid| Integer posting| BasePosting billing_line_item| [BillingLineItem](/sdk/data-billing-line-item/) amount| Decimal charged| Decimal created| DateTime modified| DateTime ### NewLineItemAdjustment Represents an adjustment applied to a billing line item. Field Name| Type ---|--- dbid| Integer posting| BasePosting billing_line_item| [BillingLineItem](/sdk/data-billing-line-item/) amount| Decimal code| String group| String deviated_from_posting_ruleset| Boolean write_off| Boolean created| DateTime modified| DateTime ### LineItemTransfer Represents a transfer of a line item balance to another coverage or patient. Field Name| Type ---|--- dbid| Integer posting| BasePosting billing_line_item| [BillingLineItem](/sdk/data-billing-line-item/) amount| Decimal code| String group| String deviated_from_posting_ruleset| Boolean transfer_to| [ClaimCoverage](/sdk/data-claim/#claimcoverage) transfer_to_patient| Boolean 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 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 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 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 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() ``` ## Attributes ### Prescription Field Name| Type ---|--- id| UUID dbid| Integer patient| [Patient](/sdk/data-patient/) note| [Note](/sdk/data-note/) 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 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) provider| [Staff](/sdk/data-staff/) status| ProcedureStatus notes| String codings| ProcedureCoding[] ### ProcedureCoding Field Name| Type ---|--- dbid| Integer 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}") ``` ## 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, picked from a date picker. `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 [Questionnaires](/sdk/questionnaires/). > **Warning:** A `DATE` answer is not readable through the data module yet. It is stored on a date column that `InterviewQuestionResponse` does not expose, so `response_option_value` is empty for a date question. Read it over the FHIR API as a `valueDate` on [QuestionnaireResponse](/api/questionnaireresponse/) in the meantime. ### 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[] ### 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 interview_responses| InterviewQuestionResponse[] ### 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)[] ### 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 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 ``` ### Committed reasons for visit The `committed` method returns records that have been committed and not entered in error: ```python from canvas_sdk.v1.data import ReasonForVisit committed = ReasonForVisit.objects.committed() ``` ### 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 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 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. 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. 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. --- # 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 ``` ## 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) 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[] ### 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) 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 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 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 --- # 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. - 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 four 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 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 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 `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 ---|--- dbid| Integer created| DateTime modified| DateTime snapshot| Snapshot image| String title| String instruction| String tag| String image_url| String (property) — 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 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 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 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") ``` `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 ---|--- 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| String 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)[] 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)[] ### StaffContactPoint Field Name| Type ---|--- id| UUID dbid| Integer 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 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 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 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 ``` ### 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 ## Computed Properties - `full_name`: The staff member's first and last name (for example, `Larry Weed`). - `credentialed_name`: The staff member's full name suffixed with their topmost credential abbreviation (for example, `Larry Weed MD`). - `top_clinical_role`: The staff member's highest-ranking clinical StaffRole, selected by privilege level when they hold more than one, or `None` if they have no clinical role. - `top_role_abbreviation`: The public credential abbreviation of the `top_clinical_role` (for example, `MD`), or `None` if there is no clinical role. - `photo_url`: The URL of the staff member's photo, if available, or a placeholder image URL. - `signature_url`: A presigned S3 URL for the staff member's signature file (valid for 1 hour), or `None` if no signature is on file. --- # 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) 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) 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 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/)[] ### 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 --- # 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)[] ### TeamContactPoint Field Name| Type ---|--- id| UUID dbid| Integer 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))) ``` ## 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. ## The document reference `UncategorizedClinicalDocument` carries the document's type, 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 record, which is also how the document 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 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) 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 ### 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. ## 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 ---|--- id| UUID dbid| Integer created| DateTime modified| DateTime patient| [Patient](/sdk/data-patient/#patient) note| [Note](/sdk/data-note/#note) image| String (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 (property) — 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/)[ 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/)[ CommandStructured units of documentation in a patient's chart. ](/sdk/data-command/)[ 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/)[ 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/)[ 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/)[ 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/)[ 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/)[ 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/)[ InvoiceA patient statement generated for a patient or guarantor, with its total, delivery method, and status. ](/sdk/data-invoice/)[ 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/)[ 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/)[ 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/)[ ReferralA referral directing a specific patient to another provider or specialist. ](/sdk/data-referral/)[ 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/)[ 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/)[ 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/ ## Overview 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 please reference "[How to set a default homepage for the provider application](/guides/set-default-homepage/)" ```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() ``` ## Structure ### **Pages** An enumeration of pages that can be set as the default homepage: Value --- `PATIENTS` `SCHEDULE` `REVENUE` `CAMPAIGNS` `DATA_INTEGRATION` ### **DefaultHomepage** A DefaultHomepage effect consists of the following properties: #### Attributes Attribute| Type| Description ---|---|--- `page`| `Pages \| None`| Optional page `application_identifier`| `str \| None`| Optional application identifier If both `page` and `application_identifier` are provided, `application_identifier` will take precedence and the default homepage will be set to the specified application. If neither is provided, the default homepage will be set to the Canvas default homepage. --- # Application Notification Badge Source: https://docs.canvasmedical.com/sdk/effect-application-notification-badge/ Notification badges let your plugin surface a count on an [application](/sdk/handlers-applications/) icon — the small number that indicates, for example, how many unread items are waiting. Badges are shown for applications scoped [`global`](/sdk/handlers-applications/#application-scopes) or [`patient_specific`](/sdk/handlers-applications/#application-scopes) — on their icon in the app drawer, or, when the application sets `show_in_panel`, on the panel alongside the other panel buttons — and for [`provider_menu_item`](/sdk/handlers-applications/#application-scopes) applications, next to their label in the provider menu. Applications in other scopes (`full_chart`, `portal_menu_item`, and the Provider Companion scopes) do not display 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 `.filter(...)` to target patients, then call `.broadcast(...)` to produce the effect. Method / Attribute| | Type| Description ---|---|---|--- `application_identifier`| required| String| Passed to the constructor. Must match the application's `class` string declared in `CANVAS_MANIFEST.json` — the `:` value (identical to the handler's `identifier`). An unknown identifier raises a validation error. `count`| required| Integer| Passed to `.broadcast()`. The badge value to display. Must be `>= 0`; a count of `0` clears the badge. `staff_ids`| optional| list[String]| Passed to `.broadcast()`. [Staff](/sdk/data-staff/) keys that should see the update. `patient_ids`| optional| list[String]| Passed to `.filter()`. [Patient](/sdk/data-patient/) keys whose chart context the update applies to. 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. set| set| The listed staff, but only while viewing the listed patients' charts. empty| empty| All staff, all patients (a system-wide update). Patients are never subscribers themselves — `patient_ids` scopes the badge to a patient's chart, where staff viewing that chart will see it. > **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) # 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. ### 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 ### 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 #### Example Usage ```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. ### 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 ### 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 #### Example Usage ```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](/sdk/examples/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. ## 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 ## 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 ## 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/ The Canvas SDK allows you to place Banners on the Canvas UI. ## Adding a Banner Alert To add a banner alert, import the `AddBannerAlert` class and create an instance of it. Attribute| | Type| Description ---|---|---|--- patient_id| required (if patient_filter is not provided)| String| The id of the [patient](/sdk/data-patient/) the alert should be associated with. patient_filter| required (if patient_id is not provided)| String| Patient queryset filters to apply the effect to multiple patients. For example, `{"active": True}` will apply to the effect to all active patients key| required| String| An identifier that categorizes the alert. narrative| required| String| The content of the alert. Maximum 90 characters. placement| required| list[Placement]| List of areas the alert should show. intent| optional| Intent| Affects the styling of the alert. href| optional| String| If given, the alert will appear as a link to this URL. ```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 Removing a banner alert is done wih the `RemoveBannerAlert` class. Create an instance of the class, identifying the key of the alert and the patient id. Return the Effect by calling the `.apply()` method. Both the `key` and `patient_id` attributes are required. ```python from canvas_sdk.effects.banner_alert import RemoveBannerAlert banner_alert = RemoveBannerAlert( key='test-alert', patient_id="d4c933fe8f6948f6a7d2a42a2641b13b", ) banner_alert.apply() ``` --- # 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. **Parameters:** Attribute| Type| Required| Description ---|---|---|--- `commands`| `list`| `true`| List of command instances to batch originate `line_number`| `int`| `false`| 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. `replace_line`| `bool`| `false`| 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`. **Returns:** An `Effect` that can be applied to originate all commands in a single operation. ## How It Works The batch originate effect processes multiple commands in a single operation: 1. **Command Preparation** : Each command in the list required 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()) diagnose.command_uuid = str(uuid4()) # One note update for all drafts, followed by a commit per command return [ BatchOriginateCommandEffect(commands=[plan1, diagnose]).apply(), plan1.commit(), diagnose.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() ] ``` ## Basic Usage ```python from canvas_sdk.commands import ( PlanCommand, HistoryOfPresentIllnessCommand, QuestionnaireCommand, DiagnoseCommand ) 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 diagnose = DiagnoseCommand() diagnose.icd10_code = "E11.9" diagnose.note_uuid = note_uuid diagnose.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, diagnose, 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, import the `AddBillingLineItem` class, create an instance of it, and return the `.apply()` method from compute. Attribute| | Type| Description ---|---|---|--- note_id| required| String| The id of the [Note](/sdk/data-note/) where the line item should be associated. cpt| required| String| The billing code to use for the line item. units| optional| Integer| The number of units to bill for the code. Defaults to `1` if not provided. assessment_ids| optional| list[String]| List of Assessment ids from the note that are relevant to the code, also referred to as "diagnosis pointers". modifiers| optional| list[Coding]| The modifiers to create with the billing code. **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. ## Updating a Billing Line Item To update a billing line item to a note, import the `UpdateBillingLineItem` class, create an instance of it, and return the `.apply()` method from compute. Attribute| | Type| Description ---|---|---|--- billing_line_item_id| required| String| The id of the [BillingLineItem](/sdk/data-billing-line-item/) to update. cpt| optional| String| The billing code to use for the line item. units| optional| Integer| The number of units to bill for the code. assessment_ids| optional| list[String]| List of Assessment ids from the note that are relevant to the code, also referred to as "diagnosis pointers". modifiers| optional| list[Coding]| The modifiers to create with the billing code. **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 to a note, import the `RemoveBillingLineItem` class, create an instance of it, and return the `.apply()` method from compute. Attribute| | Type| Description ---|---|---|--- billing_line_item_id| required| String| The id of the [BillingLineItem](/sdk/data-billing-line-item/) to update. | | | **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, 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). --- # 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` ### Attributes Attribute| Type| Description| Required ---|---|---|--- `claim_id`| `UUID` or `str`| Identifier for the claim| Yes ### Add Labels `ClaimEffect.add_labels()`: 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 `ClaimEffect.remove_labels()`: 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 `ClaimEffect.move_to_queue()`: moves a claim to a specific queue. #### 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 `ClaimEffect.add_comment()`: 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 `ClaimEffect.post_payment()`: 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. #### 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 `ClaimEffect.upsert_metadata()`: 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 `ClaimEffect.add_banner()`: adds a banner alert to a claim. Banner alerts are displayed in the UI to surface important information about a claim. #### 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 `ClaimEffect.remove_banner()`: 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 `ClaimEffect.update_provider()`: 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 `ClaimEffect.update_supervising_provider()`: 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 `ClaimEffect.set_incident_to()`: 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. ### 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 ### Implementation Details - 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]` ### 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. ## Method ### 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 DiagnoseCommand cmd = DiagnoseCommand(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. 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 The `CommandValidationErrorEffect` class accepts an optional list of `ValidationError` objects during initialization: Attribute| Type| Required| Description ---|---|---|--- `errors`| list[ValidationError]| optional| List of validation errors to be displayed to the user. ### ValidationError Each `ValidationError` object represents a single validation error message: Attribute| Type| Required| Description ---|---|---|--- `message`| String| required| The validation error message to display. Must not be empty. ### 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: - `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` - `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` - `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. > **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): - `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` - `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` - `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. ## Create Compound Medication The `CreateCompoundMedication` effect creates a new compound medication formulation in the system. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `formulation`| `str`| The compound medication formulation (max 105 characters)| Yes `potency_unit_code`| `str`| The unit of measurement for the medication| Yes `controlled_substance`| `str`| The controlled substance schedule| Yes `controlled_substance_ndc`| `str` or `None`| NDC code for controlled substances (dashes removed)| No* `active`| `bool`| Whether the compound medication is active| No *Required when `controlled_substance` is not "N" (None) ### Example Usage ```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()] ``` ## Update Compound Medication The `UpdateCompoundMedication` effect modifies an existing compound medication formulation. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `compound_medication_id`| `str`| The ID of the compound medication to update| Yes `formulation`| `str` or `None`| The compound medication formulation (max 105 characters)| No `potency_unit_code`| `str` or `None`| The unit of measurement for the medication| No `controlled_substance`| `str` or `None`| The controlled substance schedule| No `controlled_substance_ndc`| `str` or `None`| NDC code for controlled substances (dashes removed)| No `active`| `bool` or `None`| Whether the compound medication is active| No ### Example Usage ```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( compound_medication_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 effects 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. ## 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| Behaviour ---|--- `VISIBLE`| Buttons are shown and interactive (default when not listed) `HIDDEN`| Buttons are not rendered `DISABLED`| Buttons are rendered but not interactive ## Attributes Each entry in `locations` is a `LocationConfig` with the following attributes: Attribute| Required| Type| Description ---|---|---|--- `location`| yes| `Location`| The chart area to configure `visibility`| yes| `Visibility`| The visibility state for that area Top-level| Required| Type| Description ---|---|---|--- `patient_id`| yes| `str`| The patient id `locations`| no| `list[LocationConfig]`| Areas to configure. Areas not listed retain their default visible state. ## Validation Duplicate `location` values in the `locations` list will raise a `ValidationError` when `apply()` is called. ## 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`. --- # 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. ## Attributes Name| Type| Required| Description ---|---|---|--- `patient_id`| `str`| Yes| The patient's key (UUID). `content`| `str`| Yes| The C-CDA XML content as a string. Must be valid XML. `document_type`| `DocumentType`| No| Type of C-CDA document. Defaults to `DocumentType.CCD`. ## 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. ### Parameters Name| Type| Description ---|---|--- patient_id| UUID| The unique identifier of the patient. system| String| The system for the external identifier (url). value| String| The value of the external identifier. ### 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. ### Parameters Name| Type| Description ---|---|--- patient_id| `str` or `UUID`| The unique identifier of the patient. pharmacies| `list[PatientPreferredPharmacy]`| List of pharmacies to create. ### PatientPreferredPharmacy The `PatientPreferredPharmacy` dataclass represents a patient's preferred pharmacy, and if it's their default pharmacy. ### 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. #### 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.| No, defaults to `False` ### 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, import the `AssignDocumentReviewer` class from `canvas_sdk.effects.data_integration` and create an instance of it. Attribute| | Type| Description ---|---|---|--- `document_id`| required| string| The `id` of the [IntegrationTask](/sdk/data-integration-task/#integrationtask) document. `reviewer_id`| optional| string| The `id` of the [Staff](/sdk/data-staff/#staff) member to assign as reviewer. `team_id`| optional| string| The `id` of the [Team](/sdk/data-team/#team) to assign as reviewer. `review_mode`| optional| ReviewMode| Review mode. Defaults to `ReviewMode.REVIEW_REQUIRED`. `annotations`| optional| list| List of annotations for display in the UI. See Annotations. Supply either `reviewer_id` or `team_id`, not both. If both are supplied, the staff reviewer is used and the team is ignored when the Data Integration UI pre-populates the reviewer field. Supplying neither makes the effect a no-op. ### 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, import the `CategorizeDocument` class from `canvas_sdk.effects.data_integration` and create an instance of it. Attribute| | Type| Description ---|---|---|--- `document_id`| required| string| The `id` of the [IntegrationTask](/sdk/data-integration-task/#integrationtask) document to categorize. `document_type`| required| DocumentType| Document type information for categorizing the document. `annotations`| optional| list| List of annotations for display in the UI. See Annotations. ### 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, import the `LinkDocumentToPatient` class from `canvas_sdk.effects.data_integration` and create an instance of it. The plugin is responsible for matching the patient and supplying their key — the interpreter does not search for matching patients itself. Attribute| | Type| Description ---|---|---|--- `document_id`| required| string| The `id` of the [IntegrationTask](/sdk/data-integration-task/#integrationtask) document. `patient_key`| required| string| The `id` of the [Patient](/sdk/data-patient/#patient) to link the document to. `annotations`| optional| list| List of annotations for display in the UI. See Annotations. 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), import the `JunkDocument` class and create an instance of it. Attribute| | Type| Description ---|---|---|--- `document_id`| required| string| The `id` of the [IntegrationTask](/sdk/data-integration-task/#integrationtask) document to mark as junk. `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 `PrefillDocumentFields` pre-populates the parse-template fields on an IntegrationTask. **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. > The wire-level effect type for this class is `UPDATE_DOCUMENT_FIELDS`, which is what appears in event logs and the [effects table](/sdk/effects/#data-integration). The class name is `PrefillDocumentFields`. Attribute| | Type| Description ---|---|---|--- `document_id`| required| string| The `id` of the [IntegrationTask](/sdk/data-integration-task/#integrationtask) document. `templates`| required| list[PrefillTemplate]| One or more templates to prefill. Must contain at least one entry. `annotations`| optional| list| List of annotations for display in the UI. See Annotations. ### 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, import the `RemoveDocumentFromPatient` class and create an instance of it. An IntegrationTask carries at most one patient link at a time, so this effect simply clears that link. Attribute| | Type| Description ---|---|---|--- `document_id`| required| string| The `id` of the [IntegrationTask](/sdk/data-integration-task/#integrationtask) document to unlink from its patient. 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. ## 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. ## Attributes Name| Type| Description ---|---|--- `external_event_id`| `str` or `UUID` or `None`| Unique identifier of an existing external event. Must be unset when creating; required when updating. `patient_id`| `str` or `None`| ID of the patient for this event. Required when creating. `visit_identifier`| `str` or `None`| Identifier for the visit/encounter. Required when creating. `message_control_id`| `str` or `None`| Unique identifier for the message (e.g., HL7 message control ID). Required when creating. `event_type`| `str` or `None`| Type of event (e.g., "ADT^A01" for admission). Required when creating. `event_datetime`| `datetime` or `None`| Date and time when the event occurred. `event_cancelation_datetime`| `datetime` or `None`| Date and time when the event was cancelled. Set this to mark an event as cancelled. `message_datetime`| `datetime` or `None`| Date and time when the message was sent. `information_source`| `str` or `None`| Source of the event information (e.g., hospital name, system name). `facility_name`| `str` or `None`| Name of the facility where the event occurred. `raw_message`| `str` or `None`| Raw message content (e.g., original HL7 message). ## 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 ## Effect Methods ### `create()` Create a new external event record. - **Effect Type:** `CREATE_EXTERNAL_EVENT` - **Payload:** `{ "data": { patient_id, visit_identifier, message_control_id, event_type, ... } }` ### `update()` Update an existing external event. - **Effect Type:** `UPDATE_EXTERNAL_EVENT` - **Payload:** `{ "data": { external_event_id, } }` - Only fields marked dirty (modified on the model) are included in the update. ## 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 --- # 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. ## Attributes Name| Type| Required| Description ---|---|---|--- `url`| `str`| Yes| The URL to request. Must be non-empty. Cannot resolve to a private or loopback address (see Security). `method`| `HttpMethod`| No| The HTTP method to use. Defaults to `HttpMethod.GET`. `headers`| `dict[str, str]` or `None`| No| 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. `body`| `str` or `None`| No| The request body, as a string. For JSON payloads, serialize with `json.dumps(...)` and set the appropriate `Content-Type` header. `retry_on_status_codes`| `list[int]` or `None`| No| 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`. ## `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. ## Attributes Name| Type| Description ---|---|--- `reference_id`| `str` or `None`| The plugin-assigned handle (maximum 40 characters). **Required** when creating; usable as the handle for other operations. `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. `patient_id`| `str` or `None`| The [Patient](/sdk/data-patient/)'s `id`. **Required** when creating. `report_name`| `str` or `None`| Human-readable report name (maps to the report's document name). `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`. ## Methods ### create() 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() 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() 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() ``` ## 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. ## Attributes Name| Type| Description ---|---|--- `message_id`| `str` or `UUID` or `None`| Unique identifier of an existing message. Must be unset when creating a new message; required when editing. `content`| `str` or `None`| The text body of the message. Required when creating; cannot be empty. `sender_id`| `str` or `UUID`| ID of the user (Patient or Staff) who is sending the message. `recipient_id`| `str` or `UUID`| ID of the user (Patient or Staff) who will receive the message. `read`| `datetime` or `None`| Timestamp indicating when the message was read by the recipient. Defaults to `None` (unread). ## 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. ## 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. - **No Attachments Supported:** The Message effect does not yet support attachments. - **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. ## Effect Methods ### `create()` Originate a new message record without sending. - **Effect Type:** `CREATE_MESSAGE` - **Payload:** `{ "data": { content, sender_id, recipient_id } }` ### `create_and_send()` Create the message and immediately send it in one operation. - **Effect Type:** `CREATE_AND_SEND_MESSAGE` - **Payload:** `{ "data": { content, sender_id, recipient_id } }` ### `edit()` Modify an existing message's content. - **Effect Type:** `EDIT_MESSAGE` - **Payload:** `{ "data": { message_id, content?, sender_id?, recipient_id? } }` - Only fields marked dirty (modified on the model) are included; unchanged fields remain intact in the system. ### `send()` Send an already-created message. Useful if you separated creation from transmission. - **Effect Type:** `SEND_MESSAGE` - **Payload:** `{ "data": { message_id } }` ## 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() ``` --- # 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. * * * ## 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 Field| Type| Default| Description ---|---|---|--- `hide_default_state_buttons`| `bool`| `False`| Hide Canvas's native footer state-transition buttons for this note. ### 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. ## Method ### 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 ### 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 Field| Type| Default| Description ---|---|---|--- `restrict_access`| `bool`| `False`| Whether the requesting user is restricted from editing this note. `blur_content`| `bool`| `False`| Whether the note body should be blurred for the requesting user. `banner_message`| `str` | `None`| `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. ### 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 ### 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 Field| Type| Description ---|---|--- `note_id`| `str` (UUID)| The id of the note whose restrictions have changed. ### 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. ### Create Note 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 #### 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()] ``` ### Update Note 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 Sends an existing note via fax to a specified recipient. This effect allows you to transmit patient notes to external healthcare providers or facilities. #### 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 #### 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()] ``` ### Push Charges 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. ### Lock 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. ### Sign 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. #### 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()] ``` > **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 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 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 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 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 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 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 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. ### 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 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 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 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. ### 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 ### 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 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.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" return appointment_effect.update() ``` ### Reschedule Appointment 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. #### 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 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 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. ## Attributes Name| Type| Description ---|---|--- `observation_id`| `str` or `UUID` or `None`| Unique identifier of an existing observation. Must be unset when creating; required when updating. `patient_id`| `str` or `None`| ID of the patient for this observation. Required when creating. `is_member_of_id`| `str` or `UUID` or `None`| Reference to a parent observation (for grouping related observations). `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. `units`| `str` or `None`| Unit of measure for the observation value (e.g., "mmHg", "mg/dL"). `value`| `str` or `None`| The observation value as a string. `note_id`| `int` or `None`| ID of the note associated with this observation. `name`| `str` or `None`| Human-readable name for the observation. Required when creating. `effective_datetime`| `datetime` or `None`| Date and time when the observation was taken. Required when creating. `codings`| `list[CodingData]` or `None`| List of standardized codes identifying this observation (e.g., LOINC codes). `components`| `list[ObservationComponentData]` or `None`| List of components for multi-part observations (e.g., systolic and diastolic BP). `value_codings`| `list[CodingData]` or `None`| List of coded values for interpretation (e.g., "normal", "abnormal"). ## 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. ## 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. - **Effect Type:** `CREATE_OBSERVATION` - **Payload:** `{ "data": { patient_id, name, effective_datetime, ... } }` #### 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. - **Effect Type:** `UPDATE_OBSERVATION` - **Payload:** `{ "data": { observation_id, } }` - 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. - **Effect Type:** `ENTER_IN_ERROR_OBSERVATION` - **Payload:** `{ "data": { observation_id } }` - 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() ``` --- # 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. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `id`| `str` or `UUID`| ID of the patient facility address (for update/delete)| Yes (update/delete) `patient_id`| `str` or `UUID`| ID of the patient| Yes (create) `facility_id`| `str` or `UUID`| ID of an existing facility to link| Yes (if not creating new) `facility_name`| `str`| Name of new facility to create| Yes (if creating 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| Yes (if creating new facility) `facility_district`| `str`| District for new facility| No `facility_state_code`| `str`| State code for new facility (e.g., "CA", "NY")| Yes (if creating new facility) `facility_postal_code`| `str`| Postal code for new facility| Yes (if creating new facility) `room_number`| `str`| Room number at the facility| No `address_type`| `AddressType` or `str`| Type of address: "physical" or "both"| No (defaults to "physical") ## 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 ## Effect Methods ### `.create()` Creates a new patient facility address. Requires `patient_id` and either `facility_id` or facility creation fields. **Effect Type:** `CREATE_PATIENT_FACILITY_ADDRESS` ### `.update()` Updates an existing patient facility address. Requires `id` of the address to update. **Effect Type:** `UPDATE_PATIENT_FACILITY_ADDRESS` ### `.delete()` Deletes an existing patient facility address. Requires `id` of the address to delete. **Effect Type:** `DELETE_PATIENT_FACILITY_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. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `group_id`| `UUID`| The id of the [patient group](/sdk/data-patient-group/)| Yes ### 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 ### 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)])] ``` --- # 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. ## 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| Yes ## 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 ## 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. ## Excluding Note Types ### Attributes Attribute| | Type| Description ---|---|---|--- `excluded_note_types`| optional| list[str]| A list of [`NoteType.unique_identifier`](/sdk/data-note/#notetype) values (UUIDs) to exclude from the patient's timeline. Defaults to `[]`. `allowed_new_note_types`| optional| 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. 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** ### 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. ### Validation - All provided UUIDs, in either attribute, must correspond to existing [NoteType](/sdk/data-note/#notetype) records in the system. If a note type UUID does not exist, a `ValidationError` will be raised with a message indicating which note type was not found. - Values that are not valid UUIDs will also raise a `ValidationError`. --- # 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. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `first_name`| `str`| Patient's first name| Yes `last_name`| `str`| Patient's last name| Yes `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. Required for updates. Optional on creation, where it must be a 32-character hex string (a UUID4 without hyphens) — see Supplying a patient id on creation.| No `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 ## 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 - **Creation** : Creates new patient records. By default the server generates the patient id, but you may supply your own `patient_id` — see Supplying a patient id on creation - **Updates** : Updates existing patient records when `patient_id` is provided - Validates that referenced practice locations exist in the system - Verifies that referenced healthcare providers exist in the system - 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 --- # Protocol Card Source: https://docs.canvasmedical.com/sdk/effect-protocol-cards/ Protocol cards appear on the right-hand-side of a patient's chart, and can be accessed by clicking on the Protocols filter button in the filter menu. ![protocol card](/assets/images/protocol-card.png) A Protocol card consists of three main parts: - A title, which appears at the top in bold - A narrative, which appears just below the title to add any additional clarifying information - A list of recommendations, which each have a title and optionally a button that can either: - open a new tab and navigate to another site - insert commands into a note Name| Type| Required| Description ---|---|---|--- `patient_id`| _string_| `true` (if `patient_filter` is not included)| The id of the [patient](/sdk/data-patient/) `patient_filter`| _dict_| `true` (if `patient_id` is not included)| Patient queryset filters to apply the effect to multiple patients. For example, `{"active": True}` will apply to the effect to all active patients `key`| _string_| `true`| A unique identifier for the protocol card `title`| _string_| `true`| The title for the protocol card, which appears at the top in bold `narrative`| _string_| `false`| The narrative for the protocol card, which appears just below the title `can_be_snoozed`| _boolean_| `false`| Whether the protocol card can be snoozed, defaults to `false` `status`| Status| `false`| The status of the protocol card, defaults to `Status.DUE` `recommendations`| list[Recommendation]| `false`| The recommendations to appear in the protocol card `feedback_enabled`| _boolean_| `false`| Whether users can provide feedback for the protocol card in Settings, defaults to `false` `due_in`| _integer_| `false`| The number of days until the protocol card will be considered due for the patient, defaults to `-1` for already due | | | ### Recommendation Attribute| Type| Required| Description ---|---|---|--- `title`| _string_| `true`| The description of the recommendation `button`| _string_| `false`| The text to appear on the button `href`| _string_| `false`| The url for the button to navigate to `commands`| list[Command]| `false`| The commands to be inserted ### Status Enum| Value ---|--- `DUE`| due `SATISFIED`| satisfied `NOT_APPLICABLE`| not_applicable `PENDING`| pending `NOT_RELEVANT`| not_relevant | To include a command recommendation you can: - import the command from the [commands module](/sdk/commands/), instantiate the command with all the values you wish to populate, and then call `.recommend(title: str = "", button: str | None)` on the command to generate the recommendation that you can append to the protocol card's recommendations. Keep in mind that, at the moment, not all commands are supported for command insertion. See below for the list of supported commands. - instantiate the command as above, and then pass it in a list to the `commands` attribute of a recommendation.

For non-command recommendations, you can either use the `Recommendation` class, or the `.add_recommendation(title: str = "", button: str = "", href: str | None)` method on the protocol card. **Example** : ```python from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from datetime import date from canvas_sdk.effects.protocol_card import ProtocolCard, Recommendation from canvas_sdk.commands import DiagnoseCommand, PlanCommand class MyHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.PATIENT_UPDATED) def compute(self): diagnose = DiagnoseCommand( icd10_code="I10", background="feeling bad for many years", approximate_date_of_onset=date(2020, 1, 1), today_assessment="still not great", ) plan = PlanCommand( narrative="Follow up in 2 weeks", ) p = ProtocolCard( patient_id=self.target, key="testing-protocol-cards", title="This is a ProtocolCard title", narrative="this is the narrative", status=ProtocolCard.Status.DUE, recommendations=[ Recommendation(title="this recommendation has no action, just words!"), Recommendation(title="this recommendation inserts multiple commands", button="add commands", commands=[diagnose, plan]) ], ) p.add_recommendation( title="this is a recommendation", button="go here", href="https://canvasmedical.com/" ) p.recommendations.append(diagnose.recommend(title="this inserts a diagnose command")) p.recommendations.append(title="new recommendation", button="start", commands=[diagnose]) return [p.apply()] ``` To apply the effect to all active patients on plugin create and plugin update, you would include the plugin create and update events in `RESPONDS_TO`. And when responding to one of the plugin events you would use `patient_filter` instead of `patient_id` for the ProtocolCard. ```python from canvas_sdk.effects.protocol_card import ProtocolCard, Recommendation 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 datetime import date from canvas_sdk.effects.protocol_card import ProtocolCard, Recommendation from canvas_sdk.commands import DiagnoseCommand class MyHandler(BaseHandler): RESPONDS_TO = [ EventType.Name(EventType.PATIENT_UPDATED), EventType.Name(EventType.PLUGIN_CREATED), EventType.Name(EventType.PLUGIN_UPDATED), ] def compute(self): p = ProtocolCard( key="testing-protocol-cards", title="This is a ProtocolCard title", narrative="this is the narrative", can_be_snoozed=True, recommendations=[ Recommendation(title="this recommendation has no action, just words!") ], ) p.add_recommendation( title="this is a recommendation", button="go here", href="https://canvasmedical.com/" ) diagnose = DiagnoseCommand( icd10_code="I10", background="feeling bad for many years", approximate_date_of_onset=date(2020, 1, 1), today_assessment="still not great", ) p.recommendations.append(diagnose.recommend(title="this inserts a diagnose command")) if self.event.type in [EventType.PLUGIN_CREATED, EventType.PLUGIN_UPDATED]: p.patient_filter = {"active": True} else: p.patient_id = self.target return [p.apply()] ``` ### Supported Commands The following commands from the [commands module](/sdk/commands/) are currently supported for insertion from Protocol Cards: - Allergy - Assess - Diagnose - FollowUp - Goal - HistoryOfPresentIllness - Image - Immunize - Instruct - LabOrder - MedicationStatement - Perform - Plan - Prescribe - Questionnaire - ReasonForVisit - Refer - StructuredAssessment - Task - ValidateCodingGap - Vitals --- # Questionnaires Source: https://docs.canvasmedical.com/sdk/effect-questionnaires/ The Canvas SDK includes functionality for handling questionnaire-related events. ## Creating a Questionnaire Creating a questionnaire via the SDK requires current requires defining a YAML template and referencing it in your `CANVAS_MANIFEST.json` file. Read more [here](/sdk/questionnaires/). ## Creating a Questionnaire Result The `CreateQuestionnaireResult` effect allows you to create custom scoring of questionnaires in Canvas. It adds a narrative to the command in the UI and can appear in the Social Determinants section of the left side of the chart if the questionnaire is configured to show in that section (see [here](/sdk/questionnaires) for how to control setting `display_result_in_social_history_section` for questionnaires). ### Attributes Attribute| Required| Type| Description ---|---|---|--- interview_id| Yes| string| The id of the interview to associate the result with. score| Yes| float| The numerical score of the questionnaire result. abnormal| No| bool| Whether the result is considered abnormal. Defaults to `False`. narrative| No| string| A text description of the result and any recommended follow-up actions. Defaults to an empty string. code_system| Yes*| string| The code system used to identify the questionnaire (e.g., `"INTERNAL"`). code| Yes*| string| The code identifying the questionnaire within the code system (e.g., `"mchat_scoring"`). *Note: Questionnaire Results create an associated Observation record. The `code` and `code_system` fields are required in order to distinguish the Observation results. ### Example **Note:** This example assumes that an M-CHAT questionnaire created and loaded into the Canvas instance. ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.questionnaire_result import CreateQuestionnaireResult from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data.command import Command class MChatQuestionnaireResult(BaseHandler): """ Return a CreateQuestionnaireResult effect in response to a committed Questionnaire Command that contains questions coded for the M-CHAT questionnaire. """ RESPONDS_TO = [EventType.Name(EventType.QUESTIONNAIRE_COMMAND__POST_COMMIT)] MCHAT_CODE_SYSTEM = "INTERNAL" MCHAT_CODE = "mchat_scoring" def compute(self) -> list[Effect]: # Get the interview object, which will be the anchor object on the Questionnaire command. command = Command.objects.get(id=self.event.target.id) interview = command.anchor_object if not interview.committer: return [] # Return no effects if the interview has no questions that are coded as M-CHAT questions if not any( q.code == self.MCHAT_CODE and q.code_system == self.MCHAT_CODE_SYSTEM for q in interview.questionnaires.all() ): return [] # sum up the numerical value of each answered questionnaire score = 0 for response in interview.interview_responses.all(): score = score + int(response.response_option.value) # Determine the narrative and whether the result is abnormal if score >= 0 and score <= 2: abnormal = False narrative = ( "The score is LOW risk. Child has screened negative. No immediate follow-up is " "needed. However, the child should be rescreened at 24 months or after 3 months " "have passed if they are younger than 2 years. Monitoring the child's " "development remains important." ) elif score >= 3 and score <= 7: abnormal = True narrative = ( "The score is MODERATE risk. Administer the M-CHAT-R Follow-Up items that " "correspond to the at-risk responses. Only those items which were scored at risk " "need to be completed. If 2 or more items continue to be at-risk, refer the " "child immediately for (a) early intervention and (b) diagnostic evaluation." ) elif score >= 8 and score <= 20: abnormal = True narrative = ( "The score is HIGH risk. It is not necessary to complete the M-CHAT-R Follow-Up " "at this time. Bypass Follow-Up, and refer immediately for (a) early " "intervention and (b) diagnostic evaluation." ) else: abnormal = True narrative = "Error occurred trying to score questionnaire." # Create and return the effect effect = CreateQuestionnaireResult( interview_id=str(interview.id), score=score, abnormal=abnormal, narrative=narrative, code_system=self.MCHAT_CODE_SYSTEM, code=self.MCHAT_CODE, ) return [effect.apply()] ``` --- # Redirect Source: https://docs.canvasmedical.com/sdk/effect-redirect/ The `RedirectEffect` tells the Canvas frontend to navigate the browser to a destination. The plugin returns the effect from a handler and the frontend performs a full-page navigation. The headline use case is sending a user onward after a note is signed — for example, navigating back to a work queue to pick up the next patient. A redirect is delivered only to the **acting user** who triggered the handler — and only to that user's browser. Because of that, it takes effect only when the handler runs in the context of a real user with an active browser session. Return it from user-initiated handlers — a note state-change (sign/lock) handler, an action-button handler, an application handler, or an authenticated [SimpleAPI](/sdk/handlers-simple-api/) call. If a handler has no user actor — for example a `CronTask`, other background processing, or any event whose actor defaults to canvas-bot — there is no browser to navigate and the redirect is silently ignored. See [Event Actor](/sdk/events/#event-actor) for which events carry an actor. Provide **exactly one** destination: - `url` — a full URL string the plugin composes in Python (it may include patient/note ids). Either an external URL (`https://...`) or an internal Canvas path (`/panel`, `/patient/{key}?noteId=...`). - `application_id` — the identifier of a Canvas application to open. By default the navigation replaces the current tab. Set `target` to `RedirectEffect.TargetType.NEW_TAB` to open a `url` destination in a new tab instead. > **Internal navigation must be a root-relative path that starts with`/`** — e.g. `url="/schedule"` (or `/panel`, `/patient/{key}?noteId=...`). A leading-slash path is the _only_ form treated as internal navigation. A bare page name like `schedule` will **not** work: anything that doesn't start with `/` is treated as an external URL and is rejected unless it's a full `https://...` URL (protocol-relative `//...` and backslash `/\...` values are always rejected). The matching `REDIRECT_ALLOWLIST_INTERNAL` entries must likewise be leading-slash paths (e.g. `/schedule`). ## Attributes Name| Type| Required| Description ---|---|---|--- `url`| `str`| Yes*| A full external URL (`https://...`) or an internal Canvas path that **must start with`/`** (e.g. `/schedule`, `/patient/{key}`), composed by the plugin. Non-empty. `application_id`| `str`| Yes*| The identifier of a Canvas application to open. Must exist and be enabled. `target`| `TargetType`| No| Where to open a `url` destination. Defaults to `TargetType.SAME_TAB`. ***** Provide **exactly one** of `url` or `application_id` — they are mutually exclusive. ## `TargetType` A `StrEnum` of the supported navigation targets. You can also pass the string value. Member| Value| Behavior ---|---|--- `RedirectEffect.TargetType.SAME_TAB`| `"same_tab"`| Replaces the current EHR view (full-page navigation). The default. `RedirectEffect.TargetType.NEW_TAB`| `"new_tab"`| Opens the destination in a new browser tab. ## Security & Allowlist Every destination is validated **on the server** before the browser navigates — the frontend is never trusted to decide whether a target is allowed. This blocks open-redirect abuse and accidental leakage of PHI through query parameters. **Targets are denied by default.** The allowlist governs only _where a plugin may send a user_ — it does **not** change what that user is allowed to see, and cannot be used to bypass their permissions. A redirect performs an ordinary browser navigation, so the destination still enforces the user's own access: redirecting a user to a page or application they lack permission for behaves exactly as if they navigated there themselves (they're denied by that destination), and never elevates their access. The allowlist is configured **per instance by an administrator** via three plugin secrets. Your plugin declares the keys in its manifest `variables`; the admin sets each value on the Plugin admin page, or from the CLI with [`canvas config set`](/sdk/canvas_cli/#canvas-config-set). (This redirect allowlist is separate from the manifest's `url_permissions` field, which allow-lists iframe and script domains for layout effects — the two are unrelated.) Each value is a list with **one entry per line** — entries are newline-delimited, not comma-separated, because URLs and paths can legitimately contain commas (e.g. `?q=1,2,3`): Secret key| Value (one entry per line)| Permits ---|---|--- `REDIRECT_ALLOWLIST_INTERNAL`| `/patients` `/panel` `/patient`| those path roots and anything the plugin composes under them, matched at a path boundary (`/patient/{key}?noteId=...`). `REDIRECT_ALLOWLIST_EXTERNAL`| `https://app.example.com`| those origins/prefixes (**include the scheme**), matched **case-insensitively** at an origin/path boundary — so it does **not** match `https://app.example.com.evil.com`, and a differing port (e.g. `https://app.example.com:8443/...`) is not a match. `REDIRECT_ALLOWLIST_APPLICATION`| `my_plugin.applications.app:MyApp`| redirecting to those applications by id (matched exactly; the app must exist and be enabled). Declare the keys in your manifest so the admin can fill them: ```json { "variables": [ { "name": "REDIRECT_ALLOWLIST_INTERNAL" }, { "name": "REDIRECT_ALLOWLIST_EXTERNAL" }, { "name": "REDIRECT_ALLOWLIST_APPLICATION" } ] } ``` Both steps are required, and both default to "blocked": if you don't **declare** a key in the manifest, the admin has no field to fill; if the admin doesn't **set** a value, that key's allowlist is empty. An empty or absent secret allows nothing — so each redirect category (internal / external / application) only works once its key is declared _and_ an admin has given it a value. A freshly installed plugin can therefore redirect nowhere until an admin opts it in. Set a value from the CLI with your shell's newline quoting so each entry stays on its own line (see [`canvas config set`](/sdk/canvas_cli/#canvas-config-set)): ```console $ canvas config set my_plugin $'REDIRECT_ALLOWLIST_INTERNAL=/panel\n/patient' ``` Non-allowlisted destinations are dropped, and the platform logs only the plugin name and the blocked host (never the full URL/path). Protocol-relative (`//host`) and backslash (`/\host`) targets are always rejected. ## Example Usage ### Redirect to a work queue after a note is signed Requires `/panel` in the plugin's `REDIRECT_ALLOWLIST_INTERNAL` secret. ```python from canvas_sdk.effects.redirect import RedirectEffect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data.note import CurrentNoteStateEvent, NoteStates class RedirectAfterSign(BaseHandler): RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED) def compute(self): state = CurrentNoteStateEvent.objects.values_list("state", flat=True).get( id=self.event.target.id ) if state != NoteStates.LOCKED: return [] # Send the provider back to their work queue to grab the next patient. return [RedirectEffect(url="/panel").apply()] ``` ### Open an external URL in a new tab from an action button Requires `https://app.example.com` in the plugin's `REDIRECT_ALLOWLIST_EXTERNAL` secret. ```python return [ RedirectEffect( url="https://app.example.com/orders/next", target=RedirectEffect.TargetType.NEW_TAB, ).apply() ] ``` ### Redirect to an application by id Requires the identifier in the plugin's `REDIRECT_ALLOWLIST_APPLICATION` secret. ```python return [RedirectEffect(application_id="my_plugin.applications.app:MyApp").apply()] ``` ### Redirect from an application iframe An application iframe can't return an effect directly. The clean pattern is to expose a [SimpleAPI](/sdk/handlers-simple-api/) endpoint on your plugin that returns a `RedirectEffect`, and have the iframe `fetch()` it. Because a SimpleAPI request is authenticated as the acting user, the returned effect is validated and delivered through the **exact same** interpreter → allowlist → per-user path as an action-button or note-sign redirect — there is no iframe-specific code path to reason about. > **Why an API call and not`postMessage`?** An iframe could `postMessage` its parent window to request a redirect (the way the close-modal workflow does), but we recommend against it here. A redirect already has to make a server round-trip for allowlist validation, so routing the request through the parent window and a dedicated mutation would add a second mechanism that buys nothing. Having the iframe call your own API that returns the effect is cleaner: > > - **One mechanism, one mental model.** The iframe reuses the same effect pipeline as every other redirect — no separate frontend bridge, no dedicated mutation, and target validation lives in exactly one place (the interpreter). > - **Secure by construction.** The plugin whose allowlist is checked is _intrinsic_ : it's the plugin that owns the API endpoint. Nothing frontend-supplied has to be trusted or proven un-spoofable — a `postMessage` bridge would first have to attribute the message to an owning application before it could even pick which allowlist to apply. > - **Composable.** Your endpoint can do real work first — persist state, branch on the patient/note, decide _where_ to send the user — and then return the redirect alongside a normal JSON response. > **The endpoint** returns the `RedirectEffect` (optionally with a response body for the `fetch`): ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.redirect import RedirectEffect from canvas_sdk.effects.simple_api import JSONResponse, Response from canvas_sdk.handlers.simple_api import SimpleAPI, StaffSessionAuthMixin, api class MyAppAPI(StaffSessionAuthMixin, SimpleAPI): @api.post("/redirect") def redirect(self) -> list[Response | Effect]: # ...optionally do work first (persist data, decide the destination)... return [ RedirectEffect(url="/panel").apply(), # or application_id="my_plugin.applications.app:MyApp" JSONResponse({"ok": True}), ] ``` **The iframe** calls it with a credentialed, same-origin request: ```js // inside the plugin application iframe fetch('/plugin-io/api/my_plugin/redirect', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ destination: 'panel' }) }); ``` **How the redirect arrives.** The navigation does **not** come back in the `fetch()` response body. The effect is broadcast to the acting user and applied by the frontend's redirect subscription, exactly as for any other `RedirectEffect` — so the `fetch` response is just your endpoint's acknowledgement, and the browser navigates a moment later when the effect is delivered. **Requirements & gotchas** - The endpoint must authenticate the request **as a specific Canvas user** — that user is the actor the redirect targets. Use [`StaffSessionAuthMixin`](/sdk/handlers-simple-api/) (as above) or `PatientSessionAuthMixin`, which resolve the acting user from the Canvas session, or another scheme that identifies a specific user (for example an OAuth token tied to a user). A **shared-secret** scheme — `BasicAuthMixin` or `APIKeyAuthMixin` — authenticates the _request_ but establishes no acting user, so a redirect returned from it has no browser to target and is silently dropped. - For the session mixins, the request must be **same-origin and credentialed** (`credentials: 'same-origin'`) so the session is sent and the server can identify the acting user. Plugin-served iframes — rendered from `LaunchModalEffect` content or a plugin-served URL — are same-origin. An **unauthenticated** request has no acting user either, so the redirect is silently dropped. - The target still has to be **allowlisted** (see Security & Allowlist); the API path enforces the identical gate. - `target` (new tab) applies only to `url` destinations; an `application_id` always opens in-app. ## Validation Construction is validated by Pydantic and will raise a `ValidationError` for: - Providing neither `url` nor `application_id`, or providing both. - An empty `url`. - A `target` that is not a member of `TargetType`. - An `application_id` that does not resolve to an existing application. --- # Reload Action Buttons Effect Source: https://docs.canvasmedical.com/sdk/effect-reload-action-buttons/ The reload action button effects let a plugin tell Canvas to recompute and re-render its [action buttons](/sdk/handlers-action-buttons/) without the user reloading the page. This is useful after your plugin changes state that a button's `visible()` method, title, or color depends on, so the displayed buttons reflect the new state. There are two effects, one for each scope: - `ReloadNoteActionButtonsEffect` — reloads the action buttons for a single note. - `ReloadPatientActionButtonsEffect` — reloads the action buttons for a patient. Emit one from any handler's `compute()` or `handle()` — not only from an `ActionButton`. The effect re-fires the relevant [`SHOW_*_BUTTON`](/sdk/events/#action-buttons-events) events, so every button in that location recomputes `visible()` from scratch: the button set is rebuilt, not patched. ## When to reload A button's `visible()` result, its title, and its color are all computed from live data each time the location is evaluated. Reloading is how you push those changes to the footer or header without a full page refresh. Common cases: - **The button has done its job.** Once a button is clicked and its action completes, it often no longer applies — reload so its `visible()` re-evaluates to `False` and the button drops out of the set instead of lingering as a stale, re-clickable control. - **The label or color should change.** When a button reflects state — a title that shows a count of outstanding items, or a color that turns green once a task is complete — reload after that state changes so the button re-renders with its new title and color. - **Data the button depends on changed elsewhere.** After a command is committed, a note transitions to a new state, or related records are updated by another handler, reload the location so every button recomputes against the current data. * * * ## ReloadNoteActionButtonsEffect Re-evaluates the note's action buttons in the `NOTE_HEADER`, `NOTE_FOOTER`, and `NOTE_HEADER_DROPDOWN` locations. It also re-reads the note's [footer configuration](/sdk/effect-note-footer-configuration/) (by re-firing `NOTE_FOOTER__GET_CONFIGURATION`), so a plugin that toggles `hide_default_state_buttons` can refresh whether Canvas's native footer buttons are hidden without a full page reload. ### Attributes Field| Type| Description ---|---|--- `id`| `str \| UUID`| The external id of a [Note](/sdk/data-note/#note) (`Note.id`). The note must exist, or the effect raises a validation error. > **Warning:** The `note_id` carried by a [`SHOW_*_BUTTON`](/sdk/events/#action-buttons-events) context is the note's **database id** (`dbid`), while this effect is keyed by the note's **external id**. Resolve between them through the [`Note`](/sdk/data-note/#note) data model — for example `Note.objects.filter(dbid=...).first().id`. ### Example This handler reloads a note's footer whenever any command is committed, so a button that hides while the note has uncommitted commands reappears the moment the last one is committed: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.action_button import ReloadNoteActionButtonsEffect from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler from canvas_sdk.v1.data.command import Command class ReloadFooterOnCommandCommit(BaseHandler): RESPONDS_TO = [ EventType.Name(value) for value in EventType.values() if EventType.Name(value).endswith("_COMMAND__POST_COMMIT") ] def compute(self) -> list[Effect]: command = Command.objects.filter(id=self.event.target.id).first() if not command or not command.note: return [] return [ReloadNoteActionButtonsEffect(id=str(command.note.id)).apply()] ``` ## ReloadPatientActionButtonsEffect Re-evaluates the patient's action buttons in the `CHART_PATIENT_HEADER` location. ### Attributes Field| Type| Description ---|---|--- `id`| `str`| The id of a [Patient](/sdk/data-patient/#patient). The patient must exist, or the effect raises a validation error. ### Example This handler refreshes a patient's header buttons whenever one of their tasks changes, so a `CHART_PATIENT_HEADER` button that shows a live count of open tasks stays current as tasks are created or completed: ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.action_button import ReloadPatientActionButtonsEffect from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler class ReloadPatientButtonsOnTaskChange(BaseHandler): RESPONDS_TO = [ EventType.Name(EventType.TASK_CREATED), EventType.Name(EventType.TASK_UPDATED), ] def compute(self) -> list[Effect]: patient_id = (self.event.context.get("patient") or {}).get("id") if not patient_id: return [] return [ReloadPatientActionButtonsEffect(id=patient_id).apply()] ``` --- # Send Contact Verification Source: https://docs.canvasmedical.com/sdk/effect-send-contact-verification/ The `SendContactVerification` effect instructs Canvas to send a verification (for example, an email or SMS code) to a specific Patient Contact Point. Use it to verify a patient's email address or phone number — for example, before relying on that channel for outbound communications, or before enabling patient-portal features that require a verified contact channel. It applies to any patient contact point; it isn't tied to the patient portal. Attribute| Type| Description ---|---|--- `contact_point_id`| `str` or `UUID`| The id of the [`PatientContactPoint`](/sdk/effect-patient/#patientcontactpoint) to verify. ## Validation & Errors When an effect is prepared, the model validates inputs and returns structured error details if something is invalid. - **Contact Point Exists** — The effect verifies the provided `contact_point_id` maps to an existing `PatientContactPoint` record. If no matching record exists the effect will include an error detail with message: `Patient Contact Point does not exist`. ## Caveats - Emitting this effect will trigger a save to the associated `PatientContactPoint`. If your plugin sends `SendContactVerification` in direct response to a `PATIENT_CONTACT_POINT_UPDATED` event, the save triggered by the effect can cause the same event to fire again, producing an infinite event loop. To avoid this, debounce or detect origin (for example, ignore updates originating from the plugin runner or set a transient flag on the model) before emitting the effect in response to contact point update events. ## Example Usage ```python from canvas_sdk.effects import Effect from canvas_sdk.effects.send_contact_verification import SendContactVerificationEffect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class MyHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.PATIENT_CONTACT_POINT_CREATED) def compute(self) -> list[Effect]: contact_point_id = self.event.target.id verification_effect = SendContactVerificationEffect(contact_point_id=contact_point_id) return [verification_effect.apply()] ``` ## Notes - This effect only triggers a verification send for the contact point. It does not mark the contact as verified — verification completion is handled by the platform when the patient completes the challenge. - The effect relies on `PatientContactPoint` existing in the database. If your integration creates contact points in the same operation, ensure they are persisted before emitting this effect. --- # Service Provider Effects Source: https://docs.canvasmedical.com/sdk/effect-service-provider/ The Service Provider effects let a plugin build and maintain its own directory of external providers. Providers created this way are readable through the [ServiceProvider](/sdk/data-serviceprovider/) data model — where they are flagged with `is_customer_managed` — and can be offered in the provider-search surfaces by [handling those searches yourself](/guides/customize-search-results/#offering-your-own-providers-alongside-the-directory). ## Create Service Provider Creates a service provider, or updates a matching one. ### Attributes Attribute| Type| Description| Required ---|---|---|--- `first_name`| `str`| Provider name, or the organization name| Yes `specialty`| `str`| Free text| Yes `business_address`| `str`| Business address| Yes `last_name`| `str` or `None`| Omit for organizations| No `practice_name`| `str` or `None`| Practice or organization name| No `business_phone`| `str` or `None`| Business phone number| No `business_fax`| `str` or `None`| Business fax number| No `npi`| `str` or `None`| Exactly 10 digits| No `direct_address`| `str` or `None`| Up to 512 characters| No `notes`| `str` or `None`| Free-text notes| No `is_active`| `bool`| Defaults to `True`| No The required fields reject empty strings. ### Calling create more than once Creating never produces a duplicate. These four fields together identify a provider: - `first_name` - `last_name` - `specialty` - `business_address` If a provider already exists with the same values for all four, the create updates that provider rather than adding a second one. Only a provider that differs on at least one of them is created as a new record. When an existing provider is matched: - only the fields you sent are written; the rest keep their current values - a deactivated provider stays deactivated unless you send `is_active=True` Because of this, the same create is safe to run repeatedly — on a schedule, on every plugin install, or as a re-import of a directory you already loaded. An omitted or empty `last_name` is treated as the empty string when matching, so repeated creates for an organization resolve to the same record. ### Example Usage ```python from canvas_sdk.effects.service_provider import ServiceProvider from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler class ProviderLoader(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.PLUGIN_CREATED)] def compute(self): return [ ServiceProvider( first_name="Jane", last_name="Doe", specialty="Cardiology", business_address="123 Main St", business_fax="5555550100", npi="1234567890", direct_address="jane.doe@direct.example.org", ).create(), # An organization has no last name. ServiceProvider( first_name="Acme Imaging Center", specialty="Radiology", business_address="1 Hospital Way", ).create(), ] ``` ## Update Service Provider Updates the provider with the given `id`. Only the fields you set are sent, so an update never clears a field you did not mention. `first_name` and `specialty` cannot be set to `None`. ```python ServiceProvider(id="d2194110-5c9a-4842-8733-ef09ea5ead11", notes="Prefers fax").update() ``` ### Reactivating a provider Set `is_active=True` explicitly. Nothing else reactivates a provider. ```python ServiceProvider(id="d2194110-5c9a-4842-8733-ef09ea5ead11", is_active=True).update() ``` ## Deactivate Service Provider Deactivates a provider without deleting it, so anything referencing it keeps working. ```python ServiceProvider(id="d2194110-5c9a-4842-8733-ef09ea5ead11").deactivate() ``` ## Reading providers back Use the [ServiceProvider data module](/sdk/data-serviceprovider/), and `is_customer_managed` to read only the providers your plugin created: ```python from canvas_sdk.v1.data.service_provider import ServiceProvider ServiceProvider.objects.filter(is_customer_managed=True, is_active=True) ``` To surface them in the Refer, Imaging Order, fax recipient, or external care team searches, see [Offering your own providers alongside the directory](/guides/customize-search-results/#offering-your-own-providers-alongside-the-directory). To offer them in a provider search, see [`as_search_result` and `as_search_contact`](/sdk/data-serviceprovider/#search-results). --- # StaffExternalIdentifier Source: https://docs.canvasmedical.com/sdk/effect-staff-external-identifier/ Manage external identifiers on a staff member from a plugin. `StaffExternalIdentifier` is a single effect class with three methods — `.create()`, `.update()`, and `.delete()` — and which fields are required depends on the operation. ## Methods ### create() → Effect Creates a new external identifier on the specified staff member. #### Attributes Attribute| Type| Required| Description ---|---|---|--- `staff_id`| `str` / `UUID`| Yes| UUID of the [Staff](/sdk/data-staff/) record. `value`| `str`| Yes| The identifier value (e.g. an employee ID). `system`| `str`| No| The system the identifier belongs to (typically a URL). #### Validation - `staff_id` must reference an existing Staff record, or the effect raises a descriptive error. - `id` must not be set on `create()` — the UUID is assigned server-side. Supplying it fails validation. - `value` and `staff_id` are required. #### Server-side defaults Canvas applies these defaults on `create()`: - `use` → `"usual"` - `issued_date` → `"1970-01-01"` - `expiration_date` → `"2100-12-31"` #### Example ```python from canvas_sdk.effects.staff import StaffExternalIdentifier effect = StaffExternalIdentifier( staff_id="4150cd20de8a470aa570a852859ac87e", system="https://hr.example.com/", value="EMP-001234", ).create() ``` ### update() → Effect Updates fields on an existing external identifier. Only the fields you set on the effect are written; unset fields keep their existing values. #### Attributes Attribute| Type| Required| Description ---|---|---|--- `id`| `str` / `UUID`| Yes| UUID of the identifier to update. `value`| `str`| No| New identifier value. Only written if supplied. `system`| `str`| No| New system value. Only written if supplied. #### Validation - `id` is required and must reference an existing StaffExternalIdentifier record, or the effect raises a descriptive error. #### Example ```python from canvas_sdk.effects.staff import StaffExternalIdentifier effect = StaffExternalIdentifier( id="00000000-0000-0000-0000-000000000001", value="EMP-005678", ).update() ``` ### delete() → Effect Deletes the external identifier identified by `id`. #### Attributes Attribute| Type| Required| Description ---|---|---|--- `id`| `str` / `UUID`| Yes| UUID of the identifier to delete. #### Validation - `id` is required and must reference an existing StaffExternalIdentifier record, or the effect raises a descriptive error. #### Example ```python from canvas_sdk.effects.staff import StaffExternalIdentifier effect = StaffExternalIdentifier( id="00000000-0000-0000-0000-000000000001", ).delete() ``` --- # StaffMetadata Effect Source: https://docs.canvasmedical.com/sdk/effect-staff-metadata/ The `StaffMetadata` effect provides a flexible key-value storage system for staff-specific data within the Canvas system, letting plugins attach extensible information beyond the standard staff data model. ## Overview `StaffMetadata` exposes `.upsert(value)` to write or replace a metadata entry, and `.delete()` to remove one. The same key may be used across many staff members; the `(staff, key)` pair is unique per staff member. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `staff_id`| `str`| Id of the [Staff](/sdk/data-staff/) record to associate metadata with| Yes `key`| `str`| Unique identifier for the metadata entry within the staff context| Yes ## Methods ### upsert(value: str) → Effect Creates or updates a metadata entry for the specified staff 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 staff member, it will be updated with the new value. - If no entry exists, a new metadata entry will be created. - Metadata entries are isolated per staff member — the same key can hold different values for different staff members. - Values are stored as strings with no schema enforcement; the plugin is responsible for validating its own values. #### Key Naming Conventions 1. **Use descriptive names**. Choose keys that clearly indicate the purpose of the metadata. - Good: `department`, `cost_center`, `external_employee_id` - Avoid: `data1`, `temp`, `misc` 2. **Namespace your keys**. Prefix keys for integrations or modules to avoid collisions. - Example: `hr.employee_id`, `payroll.cost_center` #### Value Storage 1. **String serialization**. All values are stored as strings. For complex data: ```python import json from canvas_sdk.effects.staff_metadata import StaffMetadata metadata = StaffMetadata( staff_id="4150cd20de8a470aa570a852859ac87e", key="hr.profile", ) complex_data = {"hire_date": "2020-01-15", "department": "cardiology"} metadata.upsert(json.dumps(complex_data)) ``` 2. **Boolean values**. Store as `"true"` or `"false"` strings for consistency. #### Examples ```python from canvas_sdk.effects.staff_metadata import StaffMetadata # Tag a provider with their primary department metadata = StaffMetadata( staff_id="4150cd20de8a470aa570a852859ac87e", key="department", ) effect = metadata.upsert("cardiology") ``` Mirroring an HR system from a handler: ```python from canvas_sdk.effects.staff_metadata import StaffMetadata from canvas_sdk.handlers import BaseHandler from canvas_sdk.events import EventType class StaffHRSync(BaseHandler): """Sync select fields from an HR webhook payload onto Canvas staff.""" RESPONDS_TO = EventType.Name(EventType.STAFF_UPDATED) def compute(self): staff_id = self.event.context["staff"]["id"] hr_record = self.event.context.get("fields", {}).get("hr_record", {}) return [ StaffMetadata(staff_id=staff_id, key=f"hr.{key}").upsert(str(value)) for key, value in hr_record.items() ] ``` ### delete() → Effect Removes the metadata entry identified by `(staff_id, key)`. #### Behavior - Removes the row that matches both `staff_id` and `key`. Returns success even if no row was present (idempotent). - Does not affect other metadata entries for the same staff member with different keys. #### Example ```python from canvas_sdk.effects.staff_metadata import StaffMetadata # Clear the department tag for a staff member effect = StaffMetadata( staff_id="4150cd20de8a470aa570a852859ac87e", key="department", ).delete() ``` ## Validation The effect validates before execution: - **Staff existence** : the `staff_id` must correspond to an existing Staff record, or the effect raises a descriptive error. - **Required fields** : `staff_id` and `key` must be non-empty strings, and `.upsert(...)` requires a `value`. --- # Surescripts Effects Source: https://docs.canvasmedical.com/sdk/effect-surescripts/ > **Warning:** **This feature must be enabled by Canvas.** To use the Surescripts effects, [contact Canvas Support](https://portal.usepylon.com/canvas-medical/forms/standard) to have these Surescripts effects enabled for your instance. Until it is enabled, these effects will not send requests. Surescripts effects let plugins query insurance eligibility, benefits, and medication history through Surescripts. Eligibility and benefits requests receive responses asynchronously as corresponding events; medication history is handled by Canvas without a plugin-facing response. ## Eligibility Check a patient's insurance coverage and plan details. Send a request with `SendSurescriptsEligibilityRequestEffect`, then handle the `SURESCRIPTS_ELIGIBILITY_RESPONSE` event when the response arrives. ### SendSurescriptsEligibilityRequestEffect Sends an eligibility request to Surescripts to check a patient's insurance coverage. The response arrives as a `SURESCRIPTS_ELIGIBILITY_RESPONSE` event. #### Attributes Name| Type| Description ---|---|--- `patient_id`| `str`| The Canvas [Patient](/sdk/data-patient/#patient) ID for whom to check eligibility. `staff_id`| `str`| The Canvas [Staff](/sdk/data-staff/#staff) ID initiating the request. `correlation_id`| `str`| A unique identifier for matching the response to this request. Auto-generated if not provided. Read this value after instantiation and store it for later use. #### Correlation ID Each eligibility request includes a `correlation_id` that echoes back in the corresponding `SURESCRIPTS_ELIGIBILITY_RESPONSE` event. Use this to match responses to their originating requests when handling multiple concurrent eligibility checks. By default, the effect auto-generates a unique `correlation_id` (a UUID hex string). You can pass your own value if you need to thread external state through the request-response cycle. > **Note:** The `correlation_id` is required for receiving response events. The platform only delivers `SURESCRIPTS_ELIGIBILITY_RESPONSE` events to plugins that sent a request with a valid `correlation_id`. #### Example Usage ```python from canvas_sdk.effects.surescripts.surescripts_messages import SendSurescriptsEligibilityRequestEffect from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler class CheckEligibilityOnAppointment(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.APPOINTMENT_CREATED)] def compute(self): patient_id = self.event.target.get("id") staff_id = self.event.context.get("created_by", {}).get("id") effect = SendSurescriptsEligibilityRequestEffect( patient_id=patient_id, staff_id=staff_id, ) # Store the correlation_id to match the response later # For example, save it to custom data or cache correlation_id = effect.correlation_id return [effect.apply()] ``` ### Handling Eligibility Responses When Surescripts returns an eligibility response, the platform fires a `SURESCRIPTS_ELIGIBILITY_RESPONSE` event. Use the typed data classes from `canvas_sdk.events.surescripts` to parse the response. > **Important:** To prevent infinite loops, you cannot return a `SendSurescriptsEligibilityRequestEffect` from a handler that responds to `SURESCRIPTS_ELIGIBILITY_RESPONSE` events. #### Response Data Classes ##### SurescriptsEligibilityResponse The top-level response object containing eligibility results. Name| Type| Description ---|---|--- `correlation_id`| `str`| The correlation ID from the originating request. `patient_id`| `str`| The Canvas [Patient](/sdk/data-patient/#patient) ID for this eligibility check. `plans`| EligibilityPlan[]| List of insurance plans returned in the response. `error`| `str` or `None`| Error message if the request failed, otherwise `None`. ##### EligibilityPlan Represents a single insurance plan from the eligibility response. Name| Type| Description ---|---|--- `pbm_name`| `str`| Name of the Pharmacy Benefit Manager. `payer_id`| `str`| Identifier for the insurance payer ([Transactor](/sdk/data-coverage/#transactor)). `member_id`| `str`| The patient's member ID for this plan. `plan_network_id`| `str` or `None`| Network identifier for the plan. `group_number`| `str` or `None`| Group number for the plan. `drug_formulary_number`| `str` or `None`| Drug formulary identifier. `coverage_id`| `str` or `None`| [Coverage](/sdk/data-coverage/#coverage) identifier. `description`| `str` or `None`| Human-readable description of the plan. `rejected`| `bool`| `True` if the eligibility check was rejected for this plan. `reject_reason`| `str` or `None`| Reason for rejection, if applicable. `service_types`| `list[str]`| List of service types covered (e.g., "MEDICAL", "RX"). #### Response Handler Example ```python from canvas_sdk.events import EventType from canvas_sdk.events.surescripts import EligibilityPlan, SurescriptsEligibilityResponse from canvas_sdk.handlers.base import BaseHandler from logger import log class HandleEligibilityResponse(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.SURESCRIPTS_ELIGIBILITY_RESPONSE)] def compute(self): # Parse the event context into a typed response object response = SurescriptsEligibilityResponse.from_context(self.event.context) log.info(f"Received eligibility response for correlation_id: {response.correlation_id}") log.info(f"Patient ID: {response.patient_id}") if response.error: log.error(f"Eligibility check failed: {response.error}") return [] for plan in response.plans: if plan.rejected: log.warning(f"Plan rejected: {plan.pbm_name} - {plan.reject_reason}") else: log.info(f"Active plan: {plan.pbm_name}, Member ID: {plan.member_id}") if plan.service_types: log.info(f" Service types: {', '.join(plan.service_types)}") return [] ``` ## Benefits Retrieve formulary and coverage details for a specific medication. Send a request with `SendSurescriptsBenefitsRequestEffect`, then handle the `SURESCRIPTS_BENEFITS_RESPONSE` event when the response arrives. ### SendSurescriptsBenefitsRequestEffect Sends a benefits request to Surescripts to retrieve formulary and coverage details for a specific medication. The response arrives as a `SURESCRIPTS_BENEFITS_RESPONSE` event. #### Attributes Name| Type| Description ---|---|--- `patient_id`| `str`| The Canvas [Patient](/sdk/data-patient/#patient) ID for whom to check benefits. `staff_id`| `str`| The Canvas [Staff](/sdk/data-staff/#staff) ID initiating the request. `medication_description`| `str`| A human-readable description of the medication (e.g., "Lipitor 10 mg tablet"). `medication_ndc`| `str`| The NDC of the medication to check. `plan`| `str`| The plan or PBM to check benefits against. `correlation_id`| `str`| A unique identifier for matching the response to this request. Auto-generated if not provided. Read this value after instantiation and store it for later use. #### Correlation ID As with eligibility requests, each benefits request includes a `correlation_id` that echoes back in the corresponding `SURESCRIPTS_BENEFITS_RESPONSE` event. Use this to match responses to their originating requests when handling multiple concurrent benefits checks. By default, the effect auto-generates a unique `correlation_id` (a UUID hex string). You can pass your own value if you need to thread external state through the request-response cycle. > **Note:** The `correlation_id` is required for receiving response events. The platform only delivers `SURESCRIPTS_BENEFITS_RESPONSE` events to plugins that sent a request with a valid `correlation_id`. #### Example Usage ```python from canvas_sdk.effects.surescripts.surescripts_messages import SendSurescriptsBenefitsRequestEffect from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler class CheckBenefitsOnPrescription(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.PRESCRIBE_COMMAND__POST_ORIGINATE)] def compute(self): patient_id = self.event.target.get("id") staff_id = self.event.context.get("created_by", {}).get("id") effect = SendSurescriptsBenefitsRequestEffect( patient_id=patient_id, staff_id=staff_id, medication_description="Lipitor 10 mg tablet", medication_ndc="00071015523", plan="Acme PBM", ) # Store the correlation_id to match the response later correlation_id = effect.correlation_id return [effect.apply()] ``` ### Handling Benefits Responses When Surescripts returns a benefits response, the platform fires a `SURESCRIPTS_BENEFITS_RESPONSE` event. Use the typed data classes from `canvas_sdk.events.surescripts` to parse the response. > **Important:** To prevent infinite loops, you cannot return a `SendSurescriptsBenefitsRequestEffect` from a handler that responds to `SURESCRIPTS_BENEFITS_RESPONSE` events. #### Response Data Classes ##### SurescriptsBenefitsResponse The top-level response object containing benefits results. Name| Type| Description ---|---|--- `correlation_id`| `str`| The correlation ID from the originating request. `patient_id`| `str`| The Canvas [Patient](/sdk/data-patient/#patient) ID for this benefits check. `medication_ndc`| `str`| The NDC of the medication that was checked. `coverages`| BenefitCoverage[]| List of coverage results returned in the response. `error`| `str` or `None`| Error message if the request failed, otherwise `None`. ##### BenefitCoverage Represents a single coverage result from the benefits response. Name| Type| Description ---|---|--- `pbm_name`| `str`| Name of the Pharmacy Benefit Manager. `payer_id`| `str`| Identifier for the insurance payer ([Transactor](/sdk/data-coverage/#transactor)). `formulary_status`| `str` or `None`| Formulary status of the medication (e.g., "On Formulary"). `prior_authorization_required`| `bool`| `True` if prior authorization is required. `step_therapy_required`| `bool`| `True` if step therapy is required. `quantity_limits`| `list[str]`| Human-readable quantity limits (e.g., "30 fills per 1 calendar year"). `copays`| `list[str]`| Human-readable copay descriptions (e.g., "Tier 2: $25.00"). `alternatives`| TherapeuticAlternative[]| Therapeutic alternatives for the requested medication. `rejected`| `bool`| `True` if the benefits check was rejected for this coverage. `reject_reason`| `str` or `None`| Reason for rejection, if applicable. ##### TherapeuticAlternative Represents a therapeutic alternative suggested for the requested medication. Name| Type| Description ---|---|--- `ndc`| `str`| The NDC of the alternative medication. `description`| `str` or `None`| Human-readable description of the alternative. `brand_or_generic`| `str` or `None`| Whether the alternative is "Brand" or "Generic". `rx_or_otc`| `str` or `None`| Whether the alternative is "Rx" or "OTC". `formulary_status`| `str` or `None`| Formulary status of the alternative. `prior_authorization_required`| `bool`| `True` if prior authorization is required. `step_therapy_required`| `bool`| `True` if step therapy is required. `quantity_limits`| `list[str]`| Human-readable quantity limits. `copays`| `list[str]`| Human-readable copay descriptions. #### Response Handler Example ```python from canvas_sdk.events import EventType from canvas_sdk.events.surescripts import ( BenefitCoverage, SurescriptsBenefitsResponse, TherapeuticAlternative, ) from canvas_sdk.handlers.base import BaseHandler from logger import log class HandleBenefitsResponse(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.SURESCRIPTS_BENEFITS_RESPONSE)] def compute(self): # Parse the event context into a typed response object response = SurescriptsBenefitsResponse.from_context(self.event.context) log.info(f"Received benefits response for correlation_id: {response.correlation_id}") log.info(f"Medication NDC: {response.medication_ndc}") if response.error: log.error(f"Benefits check failed: {response.error}") return [] for coverage in response.coverages: if coverage.rejected: log.warning(f"Coverage rejected: {coverage.pbm_name} - {coverage.reject_reason}") continue log.info(f"{coverage.pbm_name} formulary status: {coverage.formulary_status}") if coverage.prior_authorization_required: log.info(" Prior authorization required") for copay in coverage.copays: log.info(f" Copay: {copay}") for alternative in coverage.alternatives: log.info(f" Alternative: {alternative.description} ({alternative.ndc})") return [] ``` ## Medication History Request a patient's medication history from Surescripts. Unlike eligibility and benefits, this effect has **no paired response event** — Canvas retrieves the medication history and processes it on the platform side; the results are not delivered back to your plugin. There is no `correlation_id` and no `SURESCRIPTS_MEDICATION_HISTORY_RESPONSE` event to handle. ### SendSurescriptsMedicationHistoryRequestEffect Sends a medication history request to Surescripts for the patient. Canvas requests the patient's recent fill history (currently the trailing 12 months). #### Attributes Name| Type| Description ---|---|--- `patient_id`| `str`| The Canvas [Patient](/sdk/data-patient/#patient) ID whose medication history to request. `staff_id`| `str`| The Canvas [Staff](/sdk/data-staff/#staff) ID initiating the request. #### Example Usage ```python from canvas_sdk.effects.surescripts.surescripts_messages import SendSurescriptsMedicationHistoryRequestEffect from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler class RequestMedicationHistoryOnAppointment(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.APPOINTMENT_CREATED)] def compute(self): patient_id = self.event.target.get("id") staff_id = self.event.context.get("created_by", {}).get("id") return [ SendSurescriptsMedicationHistoryRequestEffect( patient_id=patient_id, staff_id=staff_id, ).apply() ] ``` ## Imports ```python # Effects for sending requests from canvas_sdk.effects.surescripts.surescripts_messages import ( SendSurescriptsBenefitsRequestEffect, SendSurescriptsEligibilityRequestEffect, SendSurescriptsMedicationHistoryRequestEffect, ) # Data classes for parsing responses from canvas_sdk.events.surescripts import ( BenefitCoverage, EligibilityPlan, SurescriptsBenefitsResponse, SurescriptsEligibilityResponse, TherapeuticAlternative, ) ``` --- # TaskMetadata Effect Source: https://docs.canvasmedical.com/sdk/effect-task-metadata/ The `TaskMetadata` effect provides a flexible key-value storage system for task-specific data within the Canvas system. This effect enables the creation and updating of custom metadata entries associated with task records, allowing for extensible task information storage beyond standard task fields. ## Overview Task metadata serves as a powerful extension mechanism for storing custom task-related information that doesn't fit within the standard task data model. ## Attributes Attribute| Type| Description| Required ---|---|---|--- `task_id`| `str`| Id of the task record to associate metadata with| Yes `key`| `str`| Unique identifier for the metadata entry within the task context| Yes ## Methods ### upsert(value: str) → Effect Creates or updates a metadata entry for the specified task and key combination. #### Parameters Parameter| Type| Description| Required ---|---|---|--- `value`| `str`| The metadata value to store| Yes #### Returns An `Effect` object configured for upserting task metadata. #### Behavior - If a metadata entry with the specified key already exists for the task, 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 ## Implementation Details ### Validation The effect performs comprehensive validation before execution: 1. **Task Existence Validation** : Verifies that the referenced task exists in the system - Queries the task database to confirm the `task_id` corresponds to an existing task record - Returns a descriptive error if the task is not found 1. **Field Validation** : Ensures all required fields are provided and properly formatted - Both `task_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": { "task_id": "task-id", "key": "metadata-key", "value": "metadata-value" } } ``` ## Example Usage ### Basic Usage ```python from canvas_sdk.effects.task import TaskMetadata # Create a metadata entry for task tracking metadata = TaskMetadata( task_id="550e8400e29b41d4a716446655440000", key="external_system_id" ) # Upsert the metadata value effect = metadata.upsert("EXT-12345") ``` ### Task Integration Example ```python import json from canvas_sdk.effects.task import TaskMetadata from canvas_sdk.handlers import BaseHandler from canvas_sdk.events import EventType class TaskMetadataHandler(BaseHandler): """ Adds metadata to tasks based on task properties. """ RESPONDS_TO = EventType.Name(EventType.TASK_CREATED) def compute(self): task_id = self.context["task"]["id"] task_labels = self.context.get("task", {}).get("labels", []) effects = [] # Store task creation source metadata = TaskMetadata( task_id=task_id, key="creation_source" ) effects.append(metadata.upsert("protocol")) # Store label information as JSON if task_labels: labels_metadata = TaskMetadata( task_id=task_id, key="original_labels" ) effects.append(labels_metadata.upsert(json.dumps(task_labels))) 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`, `integration_source` - Avoid: `data1`, `temp`, `misc` 1. **Namespace Your Keys** : When building integrations or modules, prefix keys to avoid collisions - Example: `integration_task_id`, `workflow_current_stage`, `automation_trigger_id` ### 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.task import TaskMetadata metadata = TaskMetadata( task_id="550e8400e29b41d4a716446655440000", key="workflow_state" ) complex_data = {"stage": "review", "approvers": ["user1", "user2"], "timestamp": "2025-01-15T10:30:00Z"} metadata.upsert(json.dumps(complex_data)) ``` 2. **Boolean Values** : Store as "true" or "false" strings for consistency ```python from canvas_sdk.effects.task import TaskMetadata needs_followup = True metadata = TaskMetadata( task_id="550e8400e29b41d4a716446655440000", key="requires_followup" ) metadata.upsert("true" if needs_followup else "false") ``` ## Notes - Metadata entries are task-specific and isolated - the same key can have different values for different tasks - 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 --- # Tasks Source: https://docs.canvasmedical.com/sdk/effect-tasks/ The Canvas SDK includes functionality to create, update and add comments to tasks in Canvas. ## Adding a Task To add a task, import the `AddTask` class and create an instance of it. Attribute| | Type| Description ---|---|---|--- id| optional| string or UUID| Task unique UUID. If none one will be generated automatically. assignee_id| optional| string| The id of the [staff](/sdk/data-staff/) the task should be assigned to. team_id| optional| string| The id of the [team](/sdk/data-team/) the task should be assigned to. patient_id| optional| string| The id of the [patient](/sdk/data-patient/) the task is associated with. title| required| string| The title of the task. This is displayed at the top of a task card in the Canvas UI. due| optional| datetime| A date/time when the task is due. status| optional| TaskStatus| A status of OPEN, CLOSED or COMPLETED. Defaults to OPEN if not supplied. priority| optional| TaskPriority| A priority of `STAT`, `URGENT`, or `ROUTINE`. Defaults to no priority if not supplied. labels| optional| list[string]| A list of labels that will be added at the bottom of a task card in the Canvas UI. author_id| optional| string or UUID| Author's id to set task creator, defaults to CanvasBot. linked_object_id| optional| string or UUID| Linked object id of linked object. linked_object_type| optional| LinkableObjectType| Type of the LinkedObject ### Enumeration Types #### Linked Object Type Value| Description ---|--- REFERRAL| REFERRAL IMAGING| IMAGING #### TaskPriority Value| 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. An example of adding a task: ```python import arrow from canvas_sdk.effects import Effect from canvas_sdk.effects.task import AddTask, AddTaskComment, UpdateTask, TaskPriority, TaskStatus from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from canvas_sdk.v1.data.lab import LabReport from canvas_sdk.v1.data.staff import Staff from canvas_sdk.v1.data.team import Team from canvas_sdk.v1.data.referral import Referral class MyHandler(BaseHandler): RESPONDS_TO = [ EventType.Name(EventType.LAB_REPORT_CREATED), ] def compute(self) -> list[Effect]: lab_report = LabReport.objects.get(id=self.target) staff_assignee = Staff.objects.get(last_name="Weed") team = Team.objects.get(name="Labs") linked_task_type = AddTask.LinkableObjectType.REFERRAL referral = Referral.objects.get(id="d2194110-5c9a-4842-8733-ef09ea5ead11") if lab_report.patient: add_task = AddTask( assignee_id=staff_assignee.id, author_id=staff_assignee.id, team_id = team.id, patient_id=lab_report.patient.id, title="Please call the patient with their test results.", due=arrow.utcnow().shift(days=5).datetime, status=TaskStatus.OPEN, priority=TaskPriority.URGENT, labels=["call"], linked_object_id=referral.id, linked_object_type=linked_task_type, ) return [add_task.apply()] return [] ``` ## Updating a Task To update an existing task, import the `UpdateTask` class and create an instance of it. Attribute| | Type| Description ---|---|---|--- id| required| string| The id of the task being updated. assignee_id| optional| string| The id of the [staff](/sdk/data-staff/) the task should be assigned to. team_id| optional| string| The id of the [team](/sdk/data-team/) the task should be assigned to. patient_id| optional| string| The id of the [patient](/sdk/data-patient/) the task is associated with. title| optional| string| The title of the task. This is displayed at the top of a task card in the Canvas UI. due| optional| datetime| A date/time when the task is due. status| optional| TaskStatus| A status of `OPEN`, `CLOSED` or `COMPLETED`. Defaults to `OPEN` if not supplied. priority| optional| TaskPriority| A priority of `STAT`, `URGENT`, or `ROUTINE`. See TaskPriority. labels| optional| list[string]| A list of labels that will be added at the bottom of a task card in the Canvas UI. An example of updating a task to a status of `COMPLETED`: ```python from canvas_sdk.effects.task import UpdateTask, TaskStatus from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): def compute(self): update_task = UpdateTask( id="d06276ba-85c5-471b-87c0-9c9805f4ca6f", status=TaskStatus.COMPLETED, ) return [update_task.apply()] ``` ## Adding a comment to a task To add a comment to a task, import the `AddTaskComment` class and create an instance of it. Attribute| | Type| Description ---|---|---|--- task_id| required| string| The id of the task being updated. body| required| string| The comment body. author_id| optional| string or UUID| Author's id to set task comment creator, defaults to CanvasBot. ```python from canvas_sdk.effects.task import AddTaskComment from canvas_sdk.handlers.base import BaseHandler from canvas_sdk.v1.data.staff import Staff class MyHandler(BaseHandler): def compute(self): author = Staff.objects.get(last_name="Weed") add_task_comment = AddTaskComment( task_id="d06276ba-85c5-471b-87c0-9c9805f4ca6f", body="I tried to call the patient but did not get an answer.", author_id=author.id ) return [add_task_comment.apply()] ``` ## Creating a task and a comment together `AddTaskComment` requires the `task_id` of an existing task. To create a brand new task **and** add a comment to it in a single `compute()` return, supply your own `id` to `AddTask` and reuse that same value as the `task_id` on `AddTaskComment`. Because the `id` on `AddTask` is optional and is generated for you when omitted, the trick is simply to generate it yourself so you can reference it on the comment. There is no need to create the task first and listen for a follow-up event — just return both effects from the same handler, with the `AddTask` effect before the `AddTaskComment` effect. ```python import uuid import arrow from canvas_sdk.effects import Effect from canvas_sdk.effects.task import AddTask, AddTaskComment, TaskStatus from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class MyHandler(BaseHandler): RESPONDS_TO = [ EventType.Name(EventType.LAB_REPORT_CREATED), ] def compute(self) -> list[Effect]: # Generate the task id up front so the comment can reference it. task_id = str(uuid.uuid4()) add_task = AddTask( id=task_id, title="Please call the patient with their test results.", due=arrow.utcnow().shift(days=1).datetime, status=TaskStatus.OPEN, ) add_task_comment = AddTaskComment( task_id=task_id, body="Results flagged abnormal — follow up today.", ) # Order matters: the task must be created before the comment. return [add_task.apply(), add_task_comment.apply()] ``` > **Note:** The effects are applied in the order they are returned, so the `AddTask` effect must come before the `AddTaskComment` effect that references it. Both effects must be returned from the same handler — don't split them across separate handlers or plugins, and don't defer either effect, since that breaks the ordering the comment relies on. --- # Effects Source: https://docs.canvasmedical.com/sdk/effects/ Effects are instructions that plugins can return in order to perform an action in the Canvas EMR. This makes it possible to define workflows that create commands, show notifications, modify search results, etc. Effects have a `type` and a `payload`. The `type` determines the action that will be performed with the data provided in the `payload`. ## Using Effects ### Basic Usage Effects are returned as a list from the `compute` method of a plugin that inherits from `BaseHandler`. For example: ```python import json from canvas_sdk.events import EventType from canvas_sdk.effects import Effect, EffectType from canvas_sdk.handlers.base import BaseHandler class MyHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.MEDICATION_STATEMENT__MEDICATION__POST_SEARCH) def compute(self): results = self.context.get("results") post_processed_results = [] ## custom results-modifying code here ... return [ Effect( type=EffectType.AUTOCOMPLETE_SEARCH_RESULTS, payload=json.dumps(post_processed_results), ) ] ``` In the above example, the `Effect` object is constructed manually, with the `type` and `payload` set directly. Some effects have helper classes that assist you by providing payload validation and constructing the effect object for you. The example below shows the [`PatientChartSummaryConfiguration`](/sdk/layout-effect/#patient-summary) class in use: ```python from canvas_sdk.events import EventType from canvas_sdk.handlers.base import BaseHandler from canvas_sdk.effects.patient_chart_summary_configuration import PatientChartSummaryConfiguration class CustomChartLayout(BaseHandler): """ This event handler rearranges the patient summary section and hides those not used by the installation's organization. """ # This event fires when a patient's chart summary section is loading. RESPONDS_TO = EventType.Name(EventType.PATIENT_CHART_SUMMARY__SECTION_CONFIGURATION) def compute(self): layout = PatientChartSummaryConfiguration(sections=[ PatientChartSummaryConfiguration.Section.SOCIAL_DETERMINANTS, PatientChartSummaryConfiguration.Section.ALLERGIES, PatientChartSummaryConfiguration.Section.VITALS, PatientChartSummaryConfiguration.Section.MEDICATIONS, PatientChartSummaryConfiguration.Section.CONDITIONS, PatientChartSummaryConfiguration.Section.IMMUNIZATIONS, ]) return [layout.apply()] ``` ### Async Execution By default, effects returned from a plugin's `compute` method are executed inline as part of the request that triggered them. Any `Effect` can be opted into asynchronous execution by chaining `.set_async()` on it, in which case the platform will run the effect as an asynchronous task instead of inline. This is useful for effects that should run on a delay, that are tolerant of retries, or that you don't want to block the originating request. ```python from canvas_sdk.effects.claim import ClaimEffect from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class MyHandler(BaseHandler): RESPONDS_TO = EventType.Name(EventType.NOTE_STATE_CHANGE_EVENT_CREATED) def compute(self): claim_id = self.event.context["claim_id"] return [ ClaimEffect(claim_id=claim_id) .add_comment("Reviewed by automation.") .set_async(delay_seconds=60, max_retries=3) ] ``` `set_async()` returns the same `Effect` so it can be chained directly off any effect-producing call (e.g. `ClaimEffect(...).add_comment(...)`, `Response(...).apply()`, or a manually-constructed `Effect(...)`). #### Parameters Parameter| Type| Description| Required ---|---|---|--- `delay_seconds`| `int`| Number of seconds to wait before running the effect. Must be non-negative. `0` schedules the effect to run asynchronously as soon as possible.| No `max_retries`| `int`| Maximum number of retry attempts on failure. Must be non-negative. When omitted, the platform default is applied. Pass `0` to explicitly disable retries.| No Both parameters are keyword-only. Calling `.set_async()` with neither argument is a no-op and returns the effect unchanged. #### Implementation Details - Negative values or non-integer values for `delay_seconds` or `max_retries` raise `TypeError` / `ValueError`. ### Disallowed Effect/Event Combinations Canvas prevents certain combinations of events and effects to avoid infinite loops that could occur when an effect triggers the same event that generated it. The following combinations are specifically disallowed: Event Type| Disallowed Effect Types ---|--- `PATIENT_CHART__CONDITIONS`| `ADD_BANNER_ALERT` `ADD_OR_UPDATE_PROTOCOL_CARD` `PATIENT_CHART_SUMMARY__SECTION_CONFIGURATION`| `ADD_BANNER_ALERT` `ADD_OR_UPDATE_PROTOCOL_CARD` For example, if you have a plugin that responds to `PATIENT_CHART__CONDITIONS` events, you cannot return `ADD_BANNER_ALERT` or `ADD_OR_UPDATE_PROTOCOL_CARD` effects from that plugin, as this could create an infinite loop where the effect triggers another conditions event. ## Effect Classes [ Appointment LabelsProgrammatically manage labels on appointments for categorization and automation. ](/sdk/effect-appointment-labels/)[ Appointment MetadataInteract with appointment metadata. ](/sdk/effect-appointment-metadata/)[ Appointment Metadata Create FormEffect for dynamically displaying forms when scheduling an appointment. ](/sdk/appointment-metadata-create-form-effect/)[ AppointmentsCreate, update, and cancel patient appointments. ](/sdk/effect-notes/#appointment-effect)[ Application Notification BadgeDisplay and update a notification badge count on an application icon. ](/sdk/effect-application-notification-badge/)[ Banner AlertsContextual information in a patient's chart. ](/sdk/effect-banner-alerts/)[ Batch Originate CommandsEfficiently insert multiple commands into a note in a single batch operation. ](/sdk/effect-batch-originate/)[ Billing Line ItemsAdd, modify, or remove billing codes on a note. ](/sdk/effect-billing-line-items/)[ C-CDA ExportCreate a C-CDA (Consolidated Clinical Document Architecture) document for a patient. ](/sdk/effect-create-ccda-export/)[ ClaimsManage labels, update line items, move to a queue, or post a payment. ](/sdk/effect-claims/)[ Command MetadataAttach custom key-value metadata to command records. ](/sdk/effect-command-metadata/)[ Configure Command ButtonsHide or disable command buttons in specific patient chart locations. ](/sdk/effect-configure-command-buttons/)[ Command Metadata Create formAdds additional fields to commands that are stored as command metadata. ](/sdk/command-metadata-create-form-effect/)[ Command ValidationValidate commands and return structured error messages to users. ](/sdk/effect-command-validation/)[ Compound MedicationsCreate or update compound medications. ](/sdk/effect-compound-medication/)[ Create CalendarCreate a calendar for a provider. ](/sdk/calendar-create-effect/)[ Create Patient Preferred PharmaciesCreate preferred pharmacies for a patient. ](/sdk/effect-create-patient-preferred-pharmacies/)[ Custom HTML and Django TemplatesRender custom HTML using Django templates. ](/sdk/layout-effect/#custom-html-and-django-templates)[ Data IntegrationManage documents in the Data Integration queue. ](/sdk/effect-data-integration/)[ Default HomepageSet a provider's default homepage in Canvas. ](/sdk/default-homepage-effect/)[ Event Validation ErrorEffect for blocking event creation with a validation error message. ](/sdk/effect-event-validation-error/)[ External EventsCreate or update external clinical events from ADT feeds. ](/sdk/effect-external-event/)[ HTTP RequestHave the platform issue an HTTP request on behalf of a plugin. ](/sdk/effect-http-request/)[ Lab ReportCreate, update, enter-in-error, and attach results to a lab report. ](/sdk/effect-lab-report/)[ Layout EffectsModify or interact with the layout in Canvas. ](/sdk/layout-effect/)[ Manage Calendar EventsManage calendar events. ](/sdk/calendar-event-management-effects/)[ MessagesInteract with messages in Canvas. ](/sdk/effect-messages/)[ Note Footer ConfigurationConfigure a note's footer — for example, hide Canvas's default state-transition buttons. ](/sdk/effect-note-footer-configuration/)[ Note MetadataAttach custom key-value metadata to note records. ](/sdk/effect-note-metadata/)[ Note RestrictionsControl access to notes in real time — restrict editing, blur content, and show banners via plugin-driven effects. ](/sdk/effect-note-restrictions/)[ NotesInteract with notes in Canvas. ](/sdk/effect-notes/)[ ObservationsCreate or update clinical observations. ](/sdk/effect-observation/)[ PatientInteract with patient data. ](/sdk/effect-patient/)[ Patient Chart GroupEffect for grouping items on a patient chart section. ](/sdk/patient-chart-group-effect/)[ Patient Chart Summary Custom SectionServe content for a custom section in the patient chart summary. ](/sdk/patient-chart-summary-custom-section-effect/)[ Patient External IDCreate a new external identifier for a patient. ](/sdk/effect-create-patient-external-identifier/)[ Patient Facility AddressCreate, update, or delete patient facility address associations. ](/sdk/effect-patient-facility-address/)[ Patient GroupInteract with patient group data. ](/sdk/effect-patient-group/)[ Patient MetadataInteract with patient metadata. ](/sdk/effect-patient-metadata/)[ Patient Metadata Create FormEffect for dynamically displaying forms in the Patient profile. ](/sdk/patient-metadata-create-form-effect/)[ Patient PortalCustomize your Patient Portal. ](/sdk/patient-portal/)[ Patient TimelineConfigure a patient's timeline by excluding specific note types. ](/sdk/effect-patient-timeline/)[ Payment ProcessorEffects returned by custom payment processors. ](/sdk/payment-processor-effect/)[ Protocol CardsCalls to action in a patient's chart, commonly used for decision support intervention. ](/sdk/effect-protocol-cards/)[ QuestionnairesInteract with questionnaires and interviews. ](/sdk/effect-questionnaires/)[ RedirectNavigate the Canvas frontend to an allowlisted URL, page, or application. ](/sdk/effect-redirect/)[ Reload Action ButtonsRe-evaluate a note's or patient's action buttons in real time. ](/sdk/effect-reload-action-buttons/)[ Send Contact VerificationSend an email or SMS verification to a patient contact point. ](/sdk/effect-send-contact-verification/)[ ServiceProviderCreate, update, and soft-deactivate providers in a customer-managed provider directory. ](/sdk/effect-service-provider/)[ Staff External IDCreate, update, or delete an external identifier for a staff member. ](/sdk/effect-staff-external-identifier/)[ Staff MetadataInteract with staff metadata. ](/sdk/effect-staff-metadata/)[ SurescriptsQuery insurance eligibility, medication history, and benefits through Surescripts. ](/sdk/effect-surescripts/)[ Task MetadataInteract with Task metadata. ](/sdk/effect-task-metadata/)[ TasksCreate or update tasks. ](/sdk/effect-tasks/)[ CommandsThe building blocks of many end-user workflows in Canvas, including nearly all clinical workflows for documentation. ](/sdk/commands/) ## Effect Types The following effects are available to be applied in Canvas. ### Banner Alerts & Protocol Cards Effect| Description ---|--- ADD_BANNER_ALERT| Can be used to [add a banner alert](/sdk/effect-banner-alerts/#adding-a-banner-alert) to a patient's chart. REMOVE_BANNER_ALERT| Can be used to [remove a banner alert](/sdk/effect-banner-alerts/#removing-a-banner-alert) from a patient's chart. ADD_OR_UPDATE_PROTOCOL_CARD| Can be used to generate a ProtocolCard in the Canvas UI. Use the [ProtocolCard](/sdk/effect-protocol-cards/) class in the effects module. ### Layout & Navigation Effect| Description ---|--- SHOW_PATIENT_CHART_SUMMARY_SECTIONS| Can be used to reorder or hide the summary sections in a patient chart. Check out [this effect class](/sdk/layout-effect/#patient-summary). PATIENT_CHART_SUMMARY__CUSTOM_SECTION| Can be used to serve content for a custom patient chart summary section. Check out [Patient Chart Summary Custom Section](/sdk/patient-chart-summary-custom-section-effect/). SHOW_PATIENT_PROFILE_SECTIONS| Can be used to reorder or hide sections in the patient profile. Check out [Layout Effects](/sdk/layout-effect/#patient-profile). SHOW_PANEL_SECTIONS| Can be used to reorder or hide sections in the side panel. Check out [Layout Effects](/sdk/layout-effect/#panel-configuration). SHOW_PATIENT_NOTE_HEADER_DROPDOWN_SECTIONS| Can be used to hide items in the note header triple dot button dropdown. Check out [this effect class](/sdk/layout-effect/#patient-note-header-dropdown-configuration). SHOW_PROVIDER_MENU_ITEMS| Can be used to hide items in the provider (hamburger) menu. Check out [Layout Effects](/sdk/layout-effect/#provider-menu-configuration). PATIENT_CHART__GROUP_ITEMS| Can be used to group items within a specific patient chart section. Check out [Patient Chart Group](/sdk/patient-chart-group-effect/). PATIENT_TIMELINE__CONFIGURATION| Can be used to configure the patient timeline display. Check out [Patient Timeline](/sdk/effect-patient-timeline/). HOMEPAGE_CONFIGURATION| Can be used to configure the homepage layout. Check out [Default Homepage](/sdk/default-homepage-effect/). SHOW_ACTION_BUTTON| Can be used to show an action button. Check out [Action Buttons](/sdk/handlers-action-buttons/) and [LaunchModalEffects](/sdk/layout-effect/#modals). RELOAD_ACTION_BUTTONS| Can be used to refresh a note's or patient's action buttons so they re-evaluate against the latest data. Check out [Reload Action Buttons](/sdk/effect-reload-action-buttons/). SHOW_APPLICATION| Can be used to show a custom application. Check out [Applications](/sdk/handlers-applications/) and [LaunchModalEffects](/sdk/layout-effect/#modals). SET_APPLICATION_NOTIFICATION_BADGE| Can be used to display or update a notification badge count on an application icon. Check out [Application Notification Badge](/sdk/effect-application-notification-badge/). REDIRECT_CONTEXT| Returned from a [`SSO__GET_POST_LOGIN_REDIRECT`](/sdk/events/) handler to override the URL the user lands on after SAML SSO login. See [SSO Capabilities](/sdk/sso/#redirect_context). REDIRECT| Navigate the browser to an allowlisted external URL, internal Canvas page, or application from any handler (e.g. after a note is signed). Check out [Redirect](/sdk/effect-redirect/). PATIENT_CHART__CONFIGURE_COMMAND_BUTTONS| Can be used to hide or disable command buttons in specific patient chart locations. Check out [Configure Command Buttons](/sdk/effect-configure-command-buttons/). ### Search Results Effect| Description ---|--- AUTOCOMPLETE_SEARCH_RESULTS| Can be used to modify search results by re-ordering or adding text annotations to individual result records. To see how you can put this to use, check out [this guide](/guides/customize-search-results/). PATIENT_PROFILE__ADD_PHARMACY__POST_SEARCH_RESULTS| Can be used to modify pharmacy results when adding pharmacies in the patient profile. ### Annotations Check out [this guide](/guides/improve-hcc-coding-accuracy/#adding-annotations-to-conditions-and-detected-issues) for examples of using annotation effects. Effect| Description ---|--- ANNOTATE_CLAIM_CONDITION_RESULTS| Add annotations to conditions appearing in a claim's detail view. ANNOTATE_PATIENT_CHART_CONDITION_RESULTS| Add an annotation to a condition within the patient summary. ANNOTATE_PATIENT_CHART_DETECTED_ISSUE_RESULTS| Add an annotation to a detected issue within the patient summary. ### Billing Line Items Check out the [Billing Line Items](/sdk/effect-billing-line-items/) effect class documentation. Effect| Description ---|--- ADD_BILLING_LINE_ITEM| Generate a Billing Line Item in a note footer. UPDATE_BILLING_LINE_ITEM| Update an existing Billing Line Item in a note footer. REMOVE_BILLING_LINE_ITEM| Remove a Billing Line Item from a note footer. ### Tasks Check out the [Task Effects](/sdk/effect-tasks/) and [Task Metadata](/sdk/effect-task-metadata/) documentation. Effect| Description ---|--- CREATE_TASK| Create a task from a plugin. UPDATE_TASK| Update an existing task. CREATE_TASK_COMMENT| Add a comment to an existing task. UPSERT_TASK_METADATA| Add or update metadata on a task. ### Command Metadata & Validation Effect| Description ---|--- UPSERT_COMMAND_METADATA| Add or update metadata on a command. Check out [Command Metadata](/sdk/effect-command-metadata/). SET_COMMAND_CUSTOM_HTML| Set or clear custom HTML content on a staged command. Check out [set_custom_html](/sdk/commands/#set_custom_html). COMMAND_AVAILABLE_ACTIONS_RESULTS| Sort or filter command available actions. Check out [Command Actions](/sdk/commands/#command-actions). COMMAND_VALIDATION_ERRORS| Return validation errors for commands. Check out [Command Validation](/sdk/effect-command-validation/). EVENT_VALIDATION_ERROR| Return validation errors for events. Check out [Event Validation Error](/sdk/effect-event-validation-error/). BATCH_ORIGINATE_COMMANDS| Originate multiple commands in a note at once. Check out [Batch Originate](/sdk/effect-batch-originate/). COMMAND__FORM__CREATE_ADDITIONAL_FIELDS| Returns additional fields to be displayed on a command and stored as command metadata. Check out [Command Metadata Create Form](/sdk/command-metadata-create-form-effect/). ### Notes Check out the [Note Effects](/sdk/effect-notes/) documentation. Effect| Description ---|--- CREATE_NOTE| Create a note. UPDATE_NOTE| Update a note. LOCK_NOTE| Lock a note. UNLOCK_NOTE| Unlock a note. SIGN_NOTE| Sign a note. CHECK_IN_NOTE| Check in a note. NO_SHOW_NOTE| Mark a note as no-show. DELETE_NOTE| Delete a note. UNDELETE_NOTE| Restore a deleted note. DISCHARGE_NOTE| Lock and discharge an inpatient note. FAX_NOTE| Fax a note to an external recipient. PUSH_NOTE_CHARGES| Push note charges for billing. UPSERT_NOTE_METADATA| Add or update metadata on a note. GENERATE_FULL_CHART_PDF| Generate a full chart PDF for a patient. NOTE_RESTRICTIONS| Communicate whether a note is restricted for the requesting user, whether its content should be blurred, or what banner message to display. See [Note Restrictions](/sdk/effect-note-restrictions/). NOTE_RESTRICTIONS_UPDATED| Signal that note restrictions have changed, triggering an immediate real-time permission refetch on all users currently viewing that note. See [Note Restrictions](/sdk/effect-note-restrictions/). NOTE_FOOTER__CONFIGURATION| Configure a note's footer — for example, hide Canvas's default state-transition buttons so a plugin can supply its own. See [Note Footer Configuration](/sdk/effect-note-footer-configuration/). ### Appointments Check out the [Appointment Effects](/sdk/effect-notes/#appointment-effect), [Appointment Labels](/sdk/effect-appointment-labels/), and [Appointment Metadata](/sdk/effect-appointment-metadata/) documentation. Effect| Description ---|--- CREATE_APPOINTMENT| Create an appointment. UPDATE_APPOINTMENT| Update an appointment. RESCHEDULE_APPOINTMENT| Reschedule an appointment. CANCEL_APPOINTMENT| Cancel an appointment. REVERT_APPOINTMENT| Revert a cancelled, converted, or no-showed appointment back to the booked state. ADD_APPOINTMENT_LABEL| Add one or more labels to an appointment (max 3 total). REMOVE_APPOINTMENT_LABEL| Remove one or more labels from an appointment. UPSERT_APPOINTMENT_METADATA| Add or update metadata on an appointment. ### Appointment Scheduling Form Check out the [Appointment Metadata Create Form](/sdk/appointment-metadata-create-form-effect/) documentation. Effect| Description ---|--- APPOINTMENT__FORM__PROVIDERS__PRE_SEARCH_RESULTS| Modify the list of providers before a search. APPOINTMENT__FORM__LOCATIONS__PRE_SEARCH_RESULTS| Modify the list of locations before a search. APPOINTMENT__FORM__VISIT_TYPES__PRE_SEARCH_RESULTS| Modify the list of visit types before a search. APPOINTMENT__FORM__DURATIONS__PRE_SEARCH_RESULTS| Modify the list of durations before a search. APPOINTMENT__FORM__REASON_FOR_VISIT__PRE_SEARCH_RESULTS| Modify the reason for visit field before a search. APPOINTMENT__FORM__PROVIDERS__POST_SEARCH_RESULTS| Modify the list of providers after a search. APPOINTMENT__FORM__LOCATIONS__POST_SEARCH_RESULTS| Modify the list of locations after a search. APPOINTMENT__FORM__VISIT_TYPES__POST_SEARCH_RESULTS| Modify the list of visit types after a search. APPOINTMENT__FORM__DURATIONS__POST_SEARCH_RESULTS| Modify the list of durations after a search. APPOINTMENT__FORM__REASON_FOR_VISIT__POST_SEARCH_RESULTS| Modify the reason for visit field after a search. APPOINTMENT__FORM__CREATE_ADDITIONAL_FIELDS| Show additional fields on the appointment scheduling form. APPOINTMENT__SLOTS__POST_SEARCH_RESULTS| Modify slot availability when scheduling an appointment. ### Schedule Events Check out the [Schedule Event Effects](/sdk/effect-notes/#scheduleevent-effect) documentation. Effect| Description ---|--- CREATE_SCHEDULE_EVENT| Create a schedule event. UPDATE_SCHEDULE_EVENT| Update a schedule event. DELETE_SCHEDULE_EVENT| Delete a schedule event. RESCHEDULE_SCHEDULE_EVENT| Reschedule a schedule event. ### Calendar Check out the [Create Calendar](/sdk/calendar-create-effect/) and [Manage Calendar Events](/sdk/calendar-event-management-effects/) documentation. Effect| Description ---|--- CALENDAR__CREATE| Create a calendar. CALENDAR__EVENT__CREATE| Create a calendar event. CALENDAR__EVENT__UPDATE| Update a calendar event. CALENDAR__EVENT__DELETE| Delete a calendar event. ### Patients Check out the [Patient Effects](/sdk/effect-patient/), [Patient Metadata](/sdk/effect-patient-metadata/), [Patient External ID](/sdk/effect-create-patient-external-identifier/), and [Patient Facility Address](/sdk/effect-patient-facility-address/) documentation. Effect| Description ---|--- CREATE_PATIENT| Create a patient. UPDATE_PATIENT| Update a patient. UPDATE_USER| Update a user. PATIENT_METADATA__CREATE_ADDITIONAL_FIELDS| Show additional fields on the patient profile section. UPSERT_PATIENT_METADATA| Add or update metadata on a patient. CREATE_PATIENT_EXTERNAL_IDENTIFIER| Create an external identifier for a patient. CREATE_PATIENT_PREFERRED_PHARMACIES| Set preferred pharmacies for a patient. CREATE_PATIENT_FACILITY_ADDRESS| Create a facility address for a patient. UPDATE_PATIENT_FACILITY_ADDRESS| Update a facility address for a patient. DELETE_PATIENT_FACILITY_ADDRESS| Delete a facility address for a patient. ### Patient Groups Check out the [Patient Group](/sdk/effect-patient-group/) documentation. Effect| Description ---|--- PATIENT_GROUP__ADD_MEMBER| Add a member to a patient group. PATIENT_GROUP__DEACTIVATE_MEMBER| Deactivate a member from a patient group. ### Staff Effect| Description ---|--- [UPSERT_STAFF_METADATA](/sdk/effect-staff-metadata/)| Insert or update a key/value metadata entry on a staff member. [DELETE_STAFF_METADATA](/sdk/effect-staff-metadata/)| Remove a key/value metadata entry from a staff member. [CREATE_STAFF_EXTERNAL_IDENTIFIER](/sdk/effect-staff-external-identifier/)| Create a new external identifier on a staff member. [UPDATE_STAFF_EXTERNAL_IDENTIFIER](/sdk/effect-staff-external-identifier/)| Update fields on an existing external identifier. [DELETE_STAFF_EXTERNAL_IDENTIFIER](/sdk/effect-staff-external-identifier/)| Delete an external identifier from a staff member. ### Messages Check out the [Message Effects](/sdk/effect-messages/) documentation. Effect| Description ---|--- CREATE_MESSAGE| Create a message. SEND_MESSAGE| Send a message. CREATE_AND_SEND_MESSAGE| Create and send a message in one step. EDIT_MESSAGE| Edit a message. ### Observations Check out the [Observation Effects](/sdk/effect-observation/) documentation. Effect| Description ---|--- CREATE_OBSERVATION| Create an observation. UPDATE_OBSERVATION| Update an observation. ENTER_IN_ERROR_OBSERVATION| Mark an observation as entered in error. ### Lab Reports Check out the [Lab Report Effects](/sdk/effect-lab-report/) documentation. Effect| Description ---|--- CREATE_LAB_REPORT| Create a lab report decoupled from its results (no order, PDF, or values required). UPDATE_LAB_REPORT| Update lab report metadata, such as its name or effective date. ENTER_IN_ERROR_LAB_REPORT| Mark a lab report as entered in error. ATTACH_LAB_REPORT_RESULTS| Attach lab tests and values to an existing report (additive). ### Questionnaire Check out the [Questionnaire Effects](/sdk/effect-questionnaires/) documentation. Effect| Description ---|--- CREATE_QUESTIONNAIRE_RESULT| Create a questionnaire result. ### Compound Medications Check out the [Compound Medication Effects](/sdk/effect-compound-medication/) documentation. Effect| Description ---|--- CREATE_COMPOUND_MEDICATION| Create a compound medication. UPDATE_COMPOUND_MEDICATION| Update a compound medication. ### Service Providers Check out the [Service Provider Effects](/sdk/effect-service-provider/) documentation. Effect| Description ---|--- CREATE_SERVICE_PROVIDER| Create a service provider, or update a matching one. UPDATE_SERVICE_PROVIDER| Update a service provider. DEACTIVATE_SERVICE_PROVIDER| Deactivate a service provider without deleting it. ### External Events Check out the [External Event Effects](/sdk/effect-external-event/) documentation. Effect| Description ---|--- CREATE_EXTERNAL_EVENT| Create an external event. UPDATE_EXTERNAL_EVENT| Update an external event. ### CCDA Check out the [C-CDA Export](/sdk/effect-create-ccda-export/) documentation. Effect| Description ---|--- CREATE_CCDA| Create a CCDA document. ### Claims Check out the [Claims Effects](/sdk/effect-claims/) documentation. Effect| Description ---|--- POST_CLAIM_PAYMENT| Post a payment to a claim. MOVE_CLAIM_TO_QUEUE| Move a claim to a different queue. ADD_CLAIM_LABEL| Add a label to a claim. REMOVE_CLAIM_LABEL| Remove a label from a claim. ADD_CLAIM_COMMENT| Add a comment to a claim. ADD_CLAIM_BANNER_ALERT| Add a banner alert to a claim. REMOVE_CLAIM_BANNER_ALERT| Remove a banner alert from a claim. UPDATE_CLAIM_PROVIDER| Update the provider on a claim. UPSERT_CLAIM_METADATA| Add or update metadata on a claim. UPDATE_CLAIM_LINE_ITEM| Update a line item on a claim. ### Patient Portal Check out the [Patient Portal](/sdk/patient-portal/) documentation. Effect| Description ---|--- PORTAL_WIDGET| Add widgets to the patient portal landing page. SHOW_PATIENT_PORTAL_MENU_ITEMS| Configure menu items in the patient portal. Check out [Patient Portal](/sdk/patient-portal/#configure-portal-menu-items). PATIENT_PORTAL__APPLICATION_CONFIGURATION| Configure the patient portal application. PATIENT_PORTAL__FORM_RESULT| Return form results in the patient portal. PATIENT_PORTAL__APPOINTMENT_SHOW_MEETING_LINK| Show the 'join' button on the telehealth appointment card. PATIENT_PORTAL__APPOINTMENT_IS_CANCELABLE| Show the 'cancel' button on the appointment card. PATIENT_PORTAL__APPOINTMENT_IS_RESCHEDULABLE| Show the 'reschedule' button on the appointment card. PATIENT_PORTAL__SEND_INVITE| Trigger a portal invitation for a user. PATIENT_PORTAL__SEND_CONTACT_VERIFICATION| Send an email or SMS verification to a patient contact point. Works for any patient contact point, not just the portal. Check out [Send Contact Verification](/sdk/effect-send-contact-verification/). PATIENT_PORTAL__APPOINTMENTS__SLOTS__POST_SEARCH_RESULTS| Modify slot availability in the patient portal appointment scheduler. PATIENT_PORTAL__APPOINTMENTS__FORM_APPOINTMENT_TYPES__PRE_SEARCH_RESULTS| Modify appointment types in the patient portal before a search. PATIENT_PORTAL__APPOINTMENTS__FORM_APPOINTMENT_TYPES__POST_SEARCH_RESULTS| Modify appointment types in the patient portal after a search. PATIENT_PORTAL__APPOINTMENTS__FORM_LOCATIONS__PRE_SEARCH_RESULTS| Modify locations in the patient portal before a search. PATIENT_PORTAL__APPOINTMENTS__FORM_LOCATIONS__POST_SEARCH_RESULTS| Modify locations in the patient portal after a search. PATIENT_PORTAL__APPOINTMENTS__FORM_PROVIDERS__PRE_SEARCH_RESULTS| Modify providers in the patient portal before a search. PATIENT_PORTAL__APPOINTMENTS__FORM_PROVIDERS__POST_SEARCH_RESULTS| Modify providers in the patient portal after a search. ### Simple API Check out the [HTTP](/sdk/handlers-simple-api-http/) and [WebSocket](/sdk/handlers-simple-api-websocket/) SimpleAPI documentation. Effect| Description ---|--- SIMPLE_API_RESPONSE| Return a response from a SimpleAPI HTTP endpoint. SIMPLE_API_WEBSOCKET_BROADCAST| Broadcast a message to WebSocket connections. ### HTTP Requests Check out the [HTTP Request](/sdk/effect-http-request/) documentation. Effect| Description ---|--- HTTP_REQUEST| Have the platform issue an HTTP request on behalf of a plugin. Most useful when chained with [`.set_async(...)`](/sdk/effect-http-request/#async-execution) so the platform's async runner handles delay, retries, and retry-on-status-code behavior. ### Revenue / Payment Processor Effect| Description ---|--- REVENUE__PAYMENT_PROCESSOR__METADATA| Advertises a custom payment processor to Canvas. Use the [PaymentProcessorMetadata](/sdk/payment-processor-effect/#paymentprocessormetadata) class in the effects module. REVENUE__PAYMENT_PROCESSOR__FORM| Returns the HTML form used to collect and tokenize card details. Use the [PaymentProcessorForm](/sdk/payment-processor-effect/#paymentprocessorform) class in the effects module. REVENUE__PAYMENT_PROCESSOR__CREDIT_CARD_TRANSACTION| Returns the result of charging a card. Use the [CardTransaction](/sdk/payment-processor-effect/#cardtransaction) class in the effects module. REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHOD| Returns a patient's saved payment method. Use the [PaymentMethod](/sdk/payment-processor-effect/#paymentmethod) class in the effects module. REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHOD__ADD_RESPONSE| Returns the result of adding a payment method. Use the [AddPaymentMethodResponse](/sdk/payment-processor-effect/#addpaymentmethodresponse) class in the effects module. REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHOD__REMOVE_RESPONSE| Returns the result of removing a payment method. Use the [RemovePaymentMethodResponse](/sdk/payment-processor-effect/#removepaymentmethodresponse) class in the effects module. ### Surescripts Check out the [Surescripts Effects](/sdk/effect-surescripts/) documentation. Effect| Description ---|--- SEND_SURESCRIPTS_ELIGIBILITY_REQUEST| Can be used to send a Surescripts eligibility request. See [Eligibility](/sdk/effect-surescripts/#eligibility). SEND_SURESCRIPTS_BENEFITS_REQUEST| Can be used to send a Surescripts benefits request. See [Benefits](/sdk/effect-surescripts/#benefits). SEND_SURESCRIPTS_MEDICATION_HISTORY_REQUEST| Can be used to send a Surescripts medication history request. See [Medication History](/sdk/effect-surescripts/#medication-history). ### Data Integration Check out the [Data Integration Effects](/sdk/effect-data-integration/) documentation. Effect| Description ---|--- ASSIGN_DOCUMENT_REVIEWER| Assign a staff member or team as reviewer to a document in the Data Integration queue. CATEGORIZE_DOCUMENT| Categorize a document in the Data Integration queue into a specific document type. JUNK_DOCUMENT| Mark a document in the Data Integration queue as junk (spam). LINK_DOCUMENT_TO_PATIENT| Link a document in the Data Integration queue to a patient by patient id. REMOVE_DOCUMENT_FROM_PATIENT| Remove or unlink a document from a patient in the Data Integration queue. UPDATE_DOCUMENT_FIELDS| Prefill template field values on a document in the Data Integration queue (`PrefillDocumentFields` class). ### Commands Check out the [Commands documentation](/sdk/commands/) for full details. Command effects follow a consistent naming pattern: `{ACTION}_{COMMAND_TYPE}_COMMAND`. The available actions are: Action| Description ---|--- ORIGINATE| Create and open a new command in a note. Supports an optional `commit` flag to also commit the command in the same operation if the command is commit-able via SDK. EDIT| Modify field values on an existing command. DELETE| Remove an uncommitted command from a note. COMMIT| Finalize and save a command. ENTER_IN_ERROR| Mark a committed command as entered in error. SEND| Transmit a committed command to an external system (prescribe, refill, adjust prescription, lab orders only). REVIEW| Place a command into review status (prescribe, refill, adjust prescription only). DELEGATE| Delegate the order to someone else to complete (imaging order, refer only). SIGN| Sign the order (imaging order, refer only). The following command types support `ORIGINATE`, `EDIT`, `DELETE`, `COMMIT`, and `ENTER_IN_ERROR` actions unless noted otherwise: Command Type| Effect Prefix| Notes ---|---|--- Adjust Prescription| `*_ADJUST_PRESCRIPTION_COMMAND`| No COMMIT. Supports SEND and REVIEW Allergy| `*_ALLERGY_COMMAND`| Assess| `*_ASSESS_COMMAND`| Change Medication| `*_CHANGE_MEDICATION_COMMAND`| Chart Section Review| `*_CHART_SECTION_REVIEW_COMMAND`| ORIGINATE only Close Goal| `*_CLOSE_GOAL_COMMAND`| Custom Command| `*_CUSTOM_COMMAND_COMMAND`| ORIGINATE, ENTER_IN_ERROR Diagnose| `*_DIAGNOSE_COMMAND`| Exam| `*_EXAM_COMMAND`| Family History| `*_FAMILY_HISTORY_COMMAND`| Follow Up| `*_FOLLOW_UP_COMMAND`| Goal| `*_GOAL_COMMAND`| HPI| `*_HPI_COMMAND`| Imaging Order| `*_IMAGING_ORDER_COMMAND`| No COMMIT or SEND. Supports DELEGATE and SIGN Imaging Review| `*_IMAGING_REVIEW_COMMAND`| Immunization Statement| `*_IMMUNIZATION_STATEMENT_COMMAND`| Immunize| `*_IMMUNIZE_COMMAND`| Instruct| `*_INSTRUCT_COMMAND`| Lab Order| `*_LAB_ORDER_COMMAND`| Also supports SEND Lab Review| `*_LAB_REVIEW_COMMAND`| Medical History| `*_MEDICAL_HISTORY_COMMAND`| Medication Statement| `*_MEDICATION_STATEMENT_COMMAND`| Perform| `*_PERFORM_COMMAND`| Plan| `*_PLAN_COMMAND`| POC Lab Test| `*_POC_LAB_TEST_COMMAND`| Prescribe| `*_PRESCRIBE_COMMAND`| No COMMIT. Supports SEND and REVIEW Questionnaire| `*_QUESTIONNAIRE_COMMAND`| Reason For Visit| `*_REASON_FOR_VISIT_COMMAND`| ORIGINATE, EDIT, DELETE only Refer| `*_REFER_COMMAND`| No COMMIT. Supports DELEGATE and SIGN Reference| `*_REFERENCE_COMMAND`| EDIT does not refresh the rendered table Referral Review| `*_REFERRAL_REVIEW_COMMAND`| Refill| `*_REFILL_COMMAND`| No COMMIT. Supports SEND and REVIEW Remove Allergy| `*_REMOVE_ALLERGY_COMMAND`| Resolve Condition| `*_RESOLVE_CONDITION_COMMAND`| Review of Systems| `*_ROS_COMMAND`| Stop Medication| `*_STOP_MEDICATION_COMMAND`| Structured Assessment| `*_STRUCTURED_ASSESSMENT_COMMAND`| Surgical History| `*_SURGICAL_HISTORY_COMMAND`| Task| `*_TASK_COMMAND`| Uncategorized Document Review| `*_UNCATEGORIZED_DOCUMENT_REVIEW_COMMAND`| Update Diagnosis| `*_UPDATE_DIAGNOSIS_COMMAND`| Update Goal| `*_UPDATE_GOAL_COMMAND`| Vitals| `*_VITALS_COMMAND`| --- # Events Source: https://docs.canvasmedical.com/sdk/events/ **What is an Event?** An event is an occurrence of an action that happens within Canvas. For example, a patient being prescribed a medication, a user searching for a condition or an appointment being created are all examples of events. **Why should I use them?** By writing plugins that respond to events, plugin code is notified and can react to events that occur in Canvas. This enables plugin authors to create custom workflows whenever a relevant event takes place, such as making a POST request to a webhook. **How do I use them?** To make plugin code react to an event, you can add the event types listed below into the `RESPONDS_TO` list of a plugin that inherits from `BaseHandler`. For example: ```python from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler class MyHandler(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.ALLERGY_INTOLERANCE_CREATED)] def compute(self): ... ``` The plugin author can enter custom workflow code into the `compute` method that will execute every time an Allergy Intolerance is created in Canvas. For more information on writing plugins, see the guide [here](/guides/your-first-plugin/). ## Event Actor The actor is the user that initiated the event. It can be accessed within the compute method of the plugin by `self.event.actor`. For side-effect events or automated events where the action cannot be attributed to a specific user, the actor may be absent. The actor is available in the following contexts: - [**SimpleAPI**](/sdk/handlers-simple-api/) handlers — HTTP and WebSocket requests - [**Action button**](/sdk/handlers-action-buttons/) handlers — button display and click events - [**Application**](/sdk/handlers-applications/) handlers - **Note state change events** — `NOTE_STATE_CHANGE_EVENT_PRE_CREATE`, `NOTE_STATE_CHANGE_EVENT_CREATED`, `NOTE_STATE_CHANGE_EVENT_UPDATED` - **Note UI events** — `NOTE_OPENED`, `NOTE_CLOSED` - **Note restrictions events** — `GET_NOTE_RESTRICTIONS` - **Note footer events** — `NOTE_FOOTER__GET_CONFIGURATION` - **Appointment scheduling events** — all `APPOINTMENT__*` events - **Patient chart and profile events** — all `PATIENT_CHART__*` events (conditions, medications, detected issues, etc.), chart summary configuration, panel sections, and patient metadata - **Patient timeline events** — `PATIENT_TIMELINE__GET_CONFIGURATION` - **Homepage events** — `GET_HOMEPAGE_CONFIGURATION` - **Command additional-fields events** — `COMMAND__FORM__GET_ADDITIONAL_FIELDS` - **Lab order command events** — `LAB_ORDER_COMMAND__PRE_SEND`, `HEALTH_GORILLA_LAB_ORDER_PREPARED` - **Claim events** — `CLAIM__CONDITIONS` - **SSO events** — `SSO__PROCESS_ADDITIONAL_REQUEST_DATA`, `SSO__GET_POST_LOGIN_REDIRECT` - **Payment processor events** — all `REVENUE__PAYMENT_PROCESSOR__*` events - **Patient portal events** — all `PATIENT_PORTAL__*` events ```python from canvas_sdk.effects import Effect from canvas_sdk.handlers import BaseHandler from logger import log class CustomHandler(BaseHandler): RESPONDS_TO = [] def compute(self) -> list[Effect]: actor = self.event.actor log.info(actor.dbid) # The database ID of the actor, if available log.info(actor.instance) # The corresponding CanvasUser instance log.info(actor.instance.person_subclass) # The corresponding Staff or Patient instance return [] ``` ## Event Types and Context The event `target` object can be accessed within the compute method of the plugin by `self.event.target`. If `self.event.target.type` exists, it provides the same type that would be imported from the Data module. For example, a type of `Condition` would be the same as what you can import from `canvas_sdk.v1.data.condition`. The event `context` object can be accessed via `self.event.context`. The content present in each event's context depends on the event type. The table below shows what you can expect for each event type, or you could take a look yourself by logging it out. ### Common Context Patterns Many events include common contextual information to help you understand the scope and origin of the event: - **Patient context** : Most patient-related events include `"patient": {"id": pt_id}` in the context, allowing you to identify which patient the event relates to. - **Note context** : Command lifecycle events (PRE_COMMIT, POST_COMMIT, etc.) include `"note": {"uuid": note_id}` in the context, indicating the note where the command was executed. - **User context** : All command-related PRE_SEARCH and POST_SEARCH events include `"user": {"staff": staff_key}` in the context, containing the staff key of the user performing the search. This allows you to customize search results based on user-specific preferences, roles, or permissions. ```python from canvas_sdk.events import EventType from canvas_sdk.handlers import BaseHandler from logger import log class MyHandler(BaseHandler): RESPONDS_TO = [EventType.Name(EventType.ALLERGY_INTOLERANCE_CREATED)] def compute(self): log.info(self.event.context) return [] ``` ### Record lifecycle events These events fire as a result of records being created, updated, or deleted. #### Patients PATIENT_CREATED --- Occurs when a patient is created. Target object| Context object "id": pt_id "type": [Patient](/sdk/data-patient/) | empty PATIENT_UPDATED --- Occurs when a patient's data is updated. Target object| Context object "id": pt_id "type": [Patient](/sdk/data-patient/) | empty PATIENT_PREFERRED_PHARMACY_UPDATED --- Occurs when a patient's preferred pharmacy is created or updated. Target object| Context object "id": pt_id "type": [Patient](/sdk/data-patient/) | "patient": "id": pt_id CARE_TEAM_MEMBERSHIP_CREATED --- Occurs when a new care team member is added for a patient. Target object| Context object "id": care_team_membership_id "type": [CareTeamMembership](/sdk/data-care-team/#careteammembership) | "patient": "id": pt_id CARE_TEAM_MEMBERSHIP_UPDATED --- Occurs when a care team member is adjusted for a patient. Target object| Context object "id": care_team_membership_id "type": [CareTeamMembership](/sdk/data-care-team/#careteammembership) | "patient": "id": pt_id CARE_TEAM_MEMBERSHIP_DELETED --- Occurs when a care team member is removed for a patient. Target object| Context object "id": care_team_membership_id "type": [CareTeamMembership](/sdk/data-care-team/#careteammembership) | "patient": "id": pt_id PATIENT_ADDRESS_CREATED --- Occurs when an address is added for a patient. Target object| Context object "id": address_id "type": [PatientAddress](/sdk/data-patient/#patientaddress) | "patient": "id": pt_id PATIENT_ADDRESS_UPDATED --- Occurs when one of a patient's addresses is updated. Target object| Context object "id": address_id "type": [PatientAddress](/sdk/data-patient/#patientaddress) | "patient": "id": pt_id PATIENT_ADDRESS_DELETED --- Occurs when one of a patient's addresses is removed. Target object| Context object "id": address_id "type": [PatientAddress](/sdk/data-patient/#patientaddress) | "patient": "id": pt_id PATIENT_CONTACT_PERSON_CREATED --- Occurs when a contact is added for a patient. Target object| Context object "id": contact_person_id "type": None | "patient": "id": pt_id PATIENT_CONTACT_PERSON_UPDATED --- Occurs when one of a patient's contacts is updated. Target object| Context object "id": contact_person_id "type": None | "patient": "id": pt_id PATIENT_CONTACT_PERSON_DELETED --- Occurs when one of a patient's contacts is removed. Target object| Context object "id": contact_person_id "type": None | "patient": "id": pt_id PATIENT_CONTACT_POINT_CREATED --- Occurs when a contact method for a patient is added. Target object| Context object "id": contact_point_id "type": [PatientContactPoint](/sdk/data-patient/#patientcontactpoint) | "patient": "id": pt_id PATIENT_CONTACT_POINT_UPDATED --- Occurs when a contact method for a patient is updated. Target object| Context object "id": contact_point_id "type": [PatientContactPoint](/sdk/data-patient/#patientcontactpoint) | "patient": "id": pt_id PATIENT_CONTACT_POINT_DELETED --- Occurs when a contact method for a patient is removed. Target object| Context object "id": contact_point_id "type": [PatientContactPoint](/sdk/data-patient/#patientcontactpoint) | "patient": "id": pt_id PATIENT_EXTERNAL_IDENTIFIER_CREATED --- Occurs when an external identifier is created for a patient. Target object| Context object "id": patientexternalidentifier_id "type": [PatientExternalIdentifier](/sdk/data-patient/#patientexternalidentifier) | "patient": "id": pt_id PATIENT_EXTERNAL_IDENTIFIER_UPDATED --- Occurs when an external identifier for a patient is updated. Target object| Context object "id": patientexternalidentifier_id "type": [PatientExternalIdentifier](/sdk/data-patient/#patientexternalidentifier) | "patient": "id": pt_id PATIENT_EXTERNAL_IDENTIFIER_DELETED --- Occurs when an external identifier for a patient is deleted. Target object| Context object "id": patientexternalidentifier_id "type": [PatientExternalIdentifier](/sdk/data-patient/#patientexternalidentifier) | "patient": "id": pt_id #### Patient Facility Address PATIENT_FACILITY_ADDRESS_CREATED --- Occurs when a patient facility address is created. Target object| Context object "id": patientfacilityaddress_id "type": [PatientFacilityAddress](/sdk/data-patient/#patientfacilityaddress) | "patient": "id": pt_id PATIENT_FACILITY_ADDRESS_UPDATED --- Occurs when a patient facility address is updated. Target object| Context object "id": patientfacilityaddress_id "type": [PatientFacilityAddress](/sdk/data-patient/#patientfacilityaddress) | "patient": "id": pt_id PATIENT_FACILITY_ADDRESS_DELETED --- Occurs when a patient facility address is deleted. Target object| Context object "id": patientfacilityaddress_id "type": [PatientFacilityAddress](/sdk/data-patient/#patientfacilityaddress) | "patient": "id": pt_id #### Patient Metadata PATIENT_METADATA_CREATED --- Occurs when a patient's metadata is created. Target object| Context object "id": patientmetadata_id "type": [PatientMetadata](/sdk/data-patient/#patientmetadata) | "patient": "id": pt_id PATIENT_METADATA_UPDATED --- Occurs when a patient's metadata is updated. Target object| Context object "id": patientmetadata_id "type": [PatientMetadata](/sdk/data-patient/#patientmetadata) | "patient": "id": pt_id #### Allergy Intolerances ALLERGY_INTOLERANCE_CREATED --- Occurs when an allergy is created for a patient. Additional details for the allergy may become available with subsequent ALLERGY_INTOLERANCE_UPDATED events. Target object| Context object "id": allergy_id "type": [AllergyIntolerance](/sdk/data-allergy-intolerance/#allergyintolerance) | "patient": "id": pt_id ALLERGY_INTOLERANCE_UPDATED --- Occurs when an allergy is updated for a patient. Target object| Context object "id": allergy_id "type": [AllergyIntolerance](/sdk/data-allergy-intolerance/#allergyintolerance) | "patient": "id": pt_id #### Appointments APPOINTMENT_CREATED --- Occurs when an appointment is first created/booked. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "patient": "id": pt_id APPOINTMENT_UPDATED --- Occurs when details of an appointment are updated. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "patient": "id": pt_id APPOINTMENT_CHECKED_IN --- Occurs when a patient has arrived and been checked in for their appointment. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "patient": "id": pt_id APPOINTMENT_RESTORED --- Occurs when a cancelled appointment is restored to a non-cancelled status. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "patient": "id": pt_id APPOINTMENT_CANCELED --- Occurs when an appointment is cancelled. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "patient": "id": pt_id APPOINTMENT_NO_SHOWED --- Occurs when an appointment is marked as a no-show. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "patient": "id": pt_id APPOINTMENT_LABEL_ADDED --- Occurs when one or more labels are added to an appointment. Target object| Context object "id": appointment_id "type": None | "patient": "id": pt_id "label": label_name APPOINTMENT_LABEL_REMOVED --- Occurs when one or more labels are removed from an appointment. Target object| Context object "id": appointment_id "type": None | "patient": "id": pt_id "label": label_name APPOINTMENT__SLOTS__POST_SEARCH --- Occurs when requesting slot availability when scheduling an appointment. Target object| Context object | "slots_by_provider": list[dict] "selected_values": dict APPOINTMENT__FORM__PROVIDERS__PRE_SEARCH --- Occurs when a schedule appointment form is loaded. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "category": [NoteTypeCategories](/sdk/data-note/#notetypecategories) APPOINTMENT__FORM__PROVIDERS__POST_SEARCH --- Occurs when a schedule appointment form is loaded. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "providers": list[dict] "selected_values": dict "category": [NoteTypeCategories](/sdk/data-note/#notetypecategories) APPOINTMENT__FORM__LOCATIONS__PRE_SEARCH --- Occurs when a schedule appointment form is loaded. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "category": [NoteTypeCategories](/sdk/data-note/#notetypecategories) APPOINTMENT__FORM__LOCATIONS__POST_SEARCH --- Occurs when a schedule appointment form is loaded. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "locations": list[dict] "selected_values": dict "category": [NoteTypeCategories](/sdk/data-note/#notetypecategories) APPOINTMENT__FORM__VISIT_TYPES__PRE_SEARCH --- Occurs when a schedule appointment form is loaded. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "category": [NoteTypeCategories](/sdk/data-note/#notetypecategories) APPOINTMENT__FORM__VISIT_TYPES__POST_SEARCH --- Occurs when a schedule appointment form is loaded. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "visit_types": list[dict] "selected_values": dict "category": [NoteTypeCategories](/sdk/data-note/#notetypecategories) APPOINTMENT__FORM__DURATIONS__PRE_SEARCH --- Occurs when a schedule appointment form is loaded. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "category": [NoteTypeCategories](/sdk/data-note/#notetypecategories) APPOINTMENT__FORM__DURATIONS__POST_SEARCH --- Occurs when a schedule appointment form is loaded. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "durations": list[dict] "selected_values": dict "category": [NoteTypeCategories](/sdk/data-note/#notetypecategories) APPOINTMENT__FORM__REASON_FOR_VISIT__PRE_SEARCH --- Occurs when a schedule appointment form is loaded. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "category": [NoteTypeCategories](/sdk/data-note/#notetypecategories) APPOINTMENT__FORM__REASON_FOR_VISIT__POST_SEARCH --- Occurs when a schedule appointment form is loaded. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "reason_for_visit": list[dict] "selected_values": dict "category": [NoteTypeCategories](/sdk/data-note/#notetypecategories) APPOINTMENT__FORM__GET_ADDITIONAL_FIELDS --- Occurs when a schedule appointment form is loaded. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "category": [NoteTypeCategories](/sdk/data-note/#notetypecategories) APPOINTMENT__FORM__UPDATED --- Occurs when a schedule appointment form is updated. Target object| Context object "id": appointment_id "type": [Appointment](/sdk/data-appointment/#appointment) | "patient_id": int "selected_values": dict "category": [NoteTypeCategories](/sdk/data-note/#notetypecategories) #### Command Metadata COMMAND_METADATA_CREATED --- Occurs when metadata is created on a command. Target object| Context object "id": commandmetadata_id "type": [CommandMetadata](/sdk/data-command/#commandmetadata) | empty COMMAND_METADATA_UPDATED --- Occurs when metadata on a command is updated. Target object| Context object "id": commandmetadata_id "type": [CommandMetadata](/sdk/data-command/#commandmetadata) | empty #### Appointment Metadata APPOINTMENT_METADATA_CREATED --- Occurs when an appointment's metadata is created. Target object| Context object "id": appointmentmetadata_id "type": [AppointmentMetadata](/sdk/data-appointment/#appointmentmetadata) | "appointment": "id": appointment_id "patient": # present only when the appointment has a patient "id": pt_id APPOINTMENT_METADATA_UPDATED --- Occurs when an appointment's metadata is updated. Target object| Context object "id": appointmentmetadata_id "type": [AppointmentMetadata](/sdk/data-appointment/#appointmentmetadata) | "appointment": "id": appointment_id "patient": # present only when the appointment has a patient "id": pt_id #### Claims CLAIM_CREATED --- Occurs when a claim is created. Target object| Context object "id": claim_id "type": [Claim](/sdk/data-claim/#claim) | "patient": "id": pt_id "note": "uuid": note_id CLAIM_UPDATED --- Occurs when a claim is updated. Target object| Context object "id": claim_id "type": [Claim](/sdk/data-claim/#claim) | "patient": "id": pt_id "note": "uuid": note_id CLAIM_QUEUE_MOVED --- Occurs when a claim moves from one queue to another. Target object| Context object "id": claim_id "type": [Claim](/sdk/data-claim/#claim) | "patient": "id": pt_id "note": "id": note_id "queue_entered": "id": queue_id "queue_exited": "id": queue_id CLAIM__CONDITIONS --- Fires when the conditions list is loaded inside the claim summary view. Plugins can use this event to surface plugin-specific diagnosis information alongside the existing diagnosis codes on the claim. Target object| Context object "id": claim_id "type": [Claim](/sdk/data-claim/#claim) | [ "id": str, "codings": [ "code": str, "system": str, "display": str ] ] CLAIM_SUPERVISING_PROVIDER_CHANGED --- Occurs when a claim's supervising provider snapshot is created or updated. The context includes the previous value(s) of any changed fields. Target object| Context object "id": claim_id "type": [Claim](/sdk/data-claim/#claim) | "previous": null | { "first_name": str, "last_name": str, ... } CLAIM_INCIDENT_TO_CHANGED --- Occurs when a claim's `incident_to` billing flag is changed. The context includes the previous boolean value. Target object| Context object "id": claim_id "type": [Claim](/sdk/data-claim/#claim) | "previous": bool #### Billing Line Items BILLING_LINE_ITEM_CREATED --- Occurs when a billing line item is created from adding a CPT code to a note. Target object| Context object "id": billing_line_item_id "type": [BillingLineItem](/sdk/data-billing-line-item/#billinglineitem) | "patient": "id": pt_id BILLING_LINE_ITEM_UPDATED --- Occurs when a billing line item is modified. Target object| Context object "id": billing_line_item_id "type": [BillingLineItem](/sdk/data-billing-line-item/#billinglineitem) | "patient": "id": pt_id #### Calendars CALENDAR_CREATED --- Occurs when a calendar is created. Target object| Context object "id": calendar_id "type": [Calendar](/sdk/data-calendar/#calendar) | empty CALENDAR_UPDATED --- Occurs when a calendar is updated. Target object| Context object "id": calendar_id "type": [Calendar](/sdk/data-calendar/#calendar) | empty CALENDAR_DELETED --- Occurs when a calendar is deleted. Target object| Context object "id": calendar_id "type": [Calendar](/sdk/data-calendar/#calendar) | empty #### Calendar Events CALENDAR_EVENT_CREATED --- Occurs when a calendar event is created. Target object| Context object "id": event_id "type": [Event](/sdk/data-calendar/#event) | empty CALENDAR_EVENT_UPDATED --- Occurs when a calendar event is updated. Target object| Context object "id": event_id "type": [Event](/sdk/data-calendar/#event) | empty CALENDAR_EVENT_DELETED --- Occurs when a calendar event is deleted. Target object| Context object "id": event_id "type": [Event](/sdk/data-calendar/#event) | empty #### Patient Payments PATIENT_PAYMENT_PROCESSED --- Occurs when a patient payment is processed in Canvas. Target object| Context object "id": pt_id "type": [Patient](/sdk/data-patient/) | "patient_id": str "total_amount_cents": str "timestamp": str "payment_method_and_description": str "claim_payments": [ { "claim_id": str, "allocated_cents": str } ] #### Clinical Documents These events fire during the lifecycle of documents in the [Data Integration](https://canvas-medical.help.usepylon.com/articles/4617508394-data-integration) module — including inbound faxes, uploaded documents, and electronic transmissions. Each event's context includes document metadata from the underlying [IntegrationTask](/sdk/data-integration-task/). DOCUMENT_RECEIVED --- Occurs when a new clinical document is received via fax, upload, or electronic transmission. Target object| Context object "id": document_id "type": [IntegrationTask](/sdk/data-integration-task/) | "document": "id": document_id "channel": str "status": str "title": str "type": str "content_url": str "content_type": str "created_at": datetime str "patient": "id": pt_id "available_document_types": "key": str "name": str "report_type": str "template_type": str "template_fields": "name": str "label": str "type": str "required": bool DOCUMENT_LINKED_TO_PATIENT --- Occurs when a clinical document is linked to a patient. Target object| Context object "id": document_id "type": [IntegrationTask](/sdk/data-integration-task/) | "document": "id": document_id "channel": str "status": str "title": str "type": str "content_url": str "content_type": str "created_at": datetime str "patient": "id": pt_id "previous_patient": "id": pt_id "linked_at": datetime str "available_document_types": "key": str "name": str "report_type": str "template_type": str "template_fields": "name": str "label": str "type": str "required": bool DOCUMENT_CATEGORIZED --- Occurs when a clinical document is categorized. Target object| Context object "id": document_id "type": [IntegrationTask](/sdk/data-integration-task/) | "document": "id": document_id "channel": str "status": str "title": str "type": str "content_url": str "content_type": str "created_at": datetime str "document_type": "key": str "name": str "report_type": str "template_type": str "previous_document_type": "key": str "name": str "report_type": str "template_type": str "categorized_at": datetime str "patient": "id": pt_id DOCUMENT_REVIEWER_ASSIGNED --- Occurs when a reviewer (Staff or Team) is assigned or reassigned on the Data Integration document review panel. This does not fire when a reviewer is assigned on a LabReport or ImagingReport — only when the [IntegrationTaskReview](/sdk/data-integration-task/) reviewer changes. Target object| Context object "id": document_id "type": [IntegrationTask](/sdk/data-integration-task/) | "document": "id": document_id "channel": str "status": str "title": str "type": str "content_url": str "content_type": str "created_at": datetime str "assigned_at": datetime str "reviewer": "type": str "id": reviewer_id "name": str "previous_reviewer": "type": str "id": reviewer_id "name": str "patient": "id": pt_id DOCUMENT_REVIEWED --- Occurs when a clinical document is marked as reviewed. This fires when the Data Integration task status changes to reviewed, or when a Lab Results Review, Imaging Report Review, Consult Report Review, or Uncategorized Document Review command is committed. Target object| Context object "id": document_id "type": [IntegrationTask](/sdk/data-integration-task/) | "document": "id": document_id "channel": str "status": str "title": str "type": str "content_url": str "content_type": str "created_at": datetime str "review": "reviewer": "type": str "id": reviewer_id "name": str "status": str "patient_communication_method": str "internal_comment": str "message_to_patient": str "reviewed_at": datetime str "document_type": "key": str "name": str "report_type": str "template_type": str "patient": "id": pt_id DOCUMENT_DELETED --- Occurs when a document is junked/deleted from the Data Integration panel. This does not fire when a report is junked from the patient chart. Target object| Context object "id": document_id "type": [IntegrationTask](/sdk/data-integration-task/) | "document": "id": document_id "channel": str "status": str "title": str "type": str "content_url": str "content_type": str "created_at": datetime str "deleted_at": datetime str "patient": "id": pt_id "document_type": "key": str "name": str "report_type": str "template_type": str "deleted_by": "id": user_id "name": str DOCUMENT_FIELDS_UPDATED --- Occurs when a clinical document's fields are updated. This fires when a Lab Report, Imaging Report, or Specialist Consult Report is parsed and its values are saved. The `updated_fields` list contains each changed field with its new and previous values. Target object| Context object "id": document_id "type": [IntegrationTask](/sdk/data-integration-task/) | "document": "id": document_id "channel": str "status": str "title": str "type": str "content_url": str "content_type": str "created_at": datetime str "patient": "id": pt_id "updated_fields": "name": str "value": str | int | float | bool "previous_value": str | int | float | bool | None "document_type": "key": str "name": str "report_type": str "template_type": str "updated_at": datetime str #### Document Review Delegation The `DOCUMENT_DELEGATED` event fires when an uncategorized clinical document's review is delegated to another staff member or team, or routed back to its owner. It is a review-workflow event, separate from the Data Integration document-lifecycle events above. DOCUMENT_DELEGATED --- Occurs when an uncategorized clinical document review is delegated to another staff member or team from the document review surface, or routed back to its owner. This is distinct from DOCUMENT_REVIEWER_ASSIGNED, which fires only for Data Integration reviewer changes. `signature_consent` indicates whether the recipient may apply the owner's signature; `routed_back` is true when the document was returned to its owner; `comment` carries the delegator's instructions. Target object| Context object "id": document_id "type": [IntegrationTask](/sdk/data-integration-task/) | "document": [UncategorizedClinicalDocument](/sdk/data-uncategorized-clinical-document/) "id": document_id "channel": str "status": str "title": str "type": str "content_url": str "content_type": str "created_at": datetime str "delegated_at": datetime str "delegated_by": [Staff](/sdk/data-staff/#staff) "type": str ("STAFF") "id": staff_id "name": str "delegated_to": [Staff](/sdk/data-staff/#staff) or [Team](/sdk/data-team/#team) "type": str ("STAFF" or "TEAM") "id": staff_or_team_id "name": str "on_behalf_of": [Staff](/sdk/data-staff/#staff) "type": str ("STAFF") "id": staff_id "name": str "signature_consent": bool "routed_back": bool "comment": str "patient": [Patient](/sdk/data-patient/#patient) "id": pt_id #### Conditions CONDITION_ASSESSED --- Occurs when a condition is assessed through the Assess Condition command. Target object| Context object "id": condition_id "type": [Condition](/sdk/data-condition/#condition) | "patient": "id": pt_id CONDITION_CREATED --- Occurs when a condition is diagnosed for a patient. Additional details for the condition may become available with subsequent CONDITION_UPDATED events. Target object| Context object "id": condition_id "type": [Condition](/sdk/data-condition/#condition) | "patient": "id": pt_id CONDITION_RESOLVED --- Occurs when a condition is resolved through the Resolve Condition command. Target object| Context object "id": condition_id "type": [Condition](/sdk/data-condition/#condition) | "patient": "id": pt_id CONDITION_UPDATED --- Occurs when a condition is updated for a patient. Target object| Context object "id": condition_id "type": [Condition](/sdk/data-condition/#condition) | "patient": "id": pt_id #### Consents CONSENT_CREATED --- Occurs when a patient consent is created. Target object| Context object "id": consent_id "type": None | "patient": "id": pt_id CONSENT_DELETED --- Occurs when a patient consent is removed/deleted. Target object| Context object "id": consent_id "type": None | "patient": "id": pt_id CONSENT_UPDATED --- Occurs when a patient consent is updated. Target object| Context object "id": consent_id "type": None | "patient": "id": pt_id #### Coverages COVERAGE_CREATED --- Occurs when a coverage for a patient is created. Target object| Context object "id": coverage_id "type": [Coverage](/sdk/data-coverage/#coverage) | "patient": "id": pt_id COVERAGE_UPDATED --- Occurs when a coverage for a patient is updated. Target object| Context object "id": coverage_id "type": [Coverage](/sdk/data-coverage/#coverage) | "patient": "id": pt_id #### Eligibility responses A `COVERAGE_ELIGIBILITY_RESPONSE_CREATED` or `COVERAGE_ELIGIBILITY_RESPONSE_UPDATED` event fires on every eligibility response save. When the response resolves to a definite status, a matching `COVERAGE_ELIGIBILITY_RESPONSE_ACTIVE`, `COVERAGE_ELIGIBILITY_RESPONSE_INACTIVE`, or `COVERAGE_ELIGIBILITY_RESPONSE_FAILED` event fires alongside it. For example, when a failed eligibility check is first recorded, both `COVERAGE_ELIGIBILITY_RESPONSE_CREATED` and `COVERAGE_ELIGIBILITY_RESPONSE_FAILED` fire. Each event's context carries the derived `status` string and the associated `coverage`; `_FAILED` events also include the payer `errors`. COVERAGE_ELIGIBILITY_RESPONSE_CREATED --- Occurs when an eligibility response is created for a coverage. Target object| Context object "id": eligibility_response_id "type": [EligibilityResponse](/sdk/data-eligibility-response/#eligibilityresponse) | "coverage": "id": coverage_id "patient": "id": pt_id "status": str COVERAGE_ELIGIBILITY_RESPONSE_UPDATED --- Occurs when an eligibility response is updated. Target object| Context object "id": eligibility_response_id "type": [EligibilityResponse](/sdk/data-eligibility-response/#eligibilityresponse) | "coverage": "id": coverage_id "patient": "id": pt_id "status": str COVERAGE_ELIGIBILITY_RESPONSE_ACTIVE --- Occurs when an eligibility response resolves to an active status. Target object| Context object "id": eligibility_response_id "type": [EligibilityResponse](/sdk/data-eligibility-response/#eligibilityresponse) | "coverage": "id": coverage_id "patient": "id": pt_id "status": str COVERAGE_ELIGIBILITY_RESPONSE_INACTIVE --- Occurs when an eligibility response resolves to an inactive status. Target object| Context object "id": eligibility_response_id "type": [EligibilityResponse](/sdk/data-eligibility-response/#eligibilityresponse) | "coverage": "id": coverage_id "patient": "id": pt_id "status": str COVERAGE_ELIGIBILITY_RESPONSE_FAILED --- Occurs when an eligibility response check fails to complete (the payer response errored). Target object| Context object "id": eligibility_response_id "type": [EligibilityResponse](/sdk/data-eligibility-response/#eligibilityresponse) | "coverage": "id": coverage_id "patient": "id": pt_id "status": str "errors": list[str] #### Detected Issues DETECTED_ISSUE_CREATED --- Occurs when a detected issue is created. Target object| Context object "id": detected_issue_id "type": [DetectedIssue](/sdk/data-detected-issue/#detectedissue) | "patient": "id": pt_id DETECTED_ISSUE_UPDATED --- Occurs when a detected issue is updated. Target object| Context object "id": detected_issue_id "type": [DetectedIssue](/sdk/data-detected-issue/#detectedissue) | "patient": "id": pt_id DETECTED_ISSUE_EVIDENCE_CREATED --- Occurs when detected issue evidence is created. Target object| Context object "id": detected_issue_evidence_id "type": [DetectedIssueEvidence](/sdk/data-detected-issue/#detectedissueevidence) | empty DETECTED_ISSUE_EVIDENCE_UPDATED --- Occurs when a detected issue evidence is updated. Target object| Context object "id": detected_issue_evidence_id "type": [DetectedIssueEvidence](/sdk/data-detected-issue/#detectedissueevidence) | empty #### Devices DEVICE_CREATED --- Occurs when a device is created. Target object| Context object "id": device_id "type": [Device](/sdk/data-device/#device) | "patient": "id": pt_id DEVICE_UPDATED --- Occurs when a device is updated. Target object| Context object "id": device_id "type": [Device](/sdk/data-device/#device) | "patient": "id": pt_id #### Document References DOCUMENT_REFERENCE_CREATED --- Occurs when a document reference is created. Target object| Context object "id": document_reference_id "type": None | "patient": "id": pt_id DOCUMENT_REFERENCE_UPDATED --- Occurs when a document reference is updated. Target object| Context object "id": document_reference_id "type": None | "patient": "id": pt_id DOCUMENT_REFERENCE_DELETED --- Occurs when a document reference is deleted. Target object| Context object "id": document_reference_id "type": None | "patient": "id": pt_id #### Encounters ENCOUNTER_CREATED --- Occurs when an encounter is created. Target object| Context object "id": encounter_id "type": [Encounter](/sdk/data-encounter/#encounter) | empty ENCOUNTER_UPDATED --- Occurs when an encounter is updated. Target object| Context object "id": encounter_id "type": [Encounter](/sdk/data-encounter/#encounter) | empty #### Imaging Reports IMAGING_REPORT_CREATED --- Occurs when an imaging report is entered into the data integration section of canvas. Target object| Context object "id": report_id "type": [ImagingReport](/sdk/data-imaging/#imagingreport) | "patient": "id": pt_id IMAGING_REPORT_UPDATED --- Occurs when an imaging report is updated. Target object| Context object "id": report_id "type": [ImagingReport](/sdk/data-imaging/#imagingreport) | "patient": "id": pt_id #### Immunizations IMMUNIZATION_CREATED --- Occurs when an immunization is created. Additional details for the immunization may become available with subsequent IMMUNIZATION_STATEMENT_UPDATED events. Target object| Context object "id": immunization_id "type": [Immunization](/sdk/data-immunization/#immunization) | "patient": "id": pt_id IMMUNIZATION_UPDATED --- Occurs when an immunization is updated. Target object| Context object "id": immunization_id "type": [Immunization](/sdk/data-immunization/#immunization) | "patient": "id": pt_id IMMUNIZATION_STATEMENT_CREATED --- Occurs when an immunization statement is created. Additional details for the immunization statement may become available with subsequent IMMUNIZATION_STATEMENT_UPDATED events. Target object| Context object "id": immunization_id "type": [Immunization](/sdk/data-immunization/#immunization) | "patient": "id": pt_id IMMUNIZATION_STATEMENT_UPDATED --- Occurs when an immunization statement is updated. Target object| Context object "id": immunization_id "type": [Immunization](/sdk/data-immunization/#immunization) | "patient": "id": pt_id #### Instructions INSTRUCTION_CREATED --- Occurs when an instruction is created using the Instruct command. Additional details for the instruction may become available with subsequent INSTRUCTION_UPDATED events. Target object| Context object "id": instruction_id "type": None | "patient": "id": pt_id INSTRUCTION_UPDATED --- Occurs when an instruction is updated. Target object| Context object "id": instruction_id "type": None | "patient": "id": pt_id #### Interviews INTERVIEW_CREATED --- Occurs when an interview is created using the Questionnaire command or through the Questionnaire endpoint in the FHIR API. Additional details for the interview may become available with subsequent INTERVIEW_UPDATED events. Target object| Context object "id": interview_id "type": [Interview](/sdk/data-questionnaire/#interview) | "patient": "id": pt_id INTERVIEW_UPDATED --- Occurs when an interview is updated. Target object| Context object "id": interview_id "type": [Interview](/sdk/data-questionnaire/#interview) | "patient": "id": pt_id #### Labs LAB_ORDER_CREATED --- Occurs when a lab order is created via the Lab Order command. Additional details for the lab order may become available with subsequent LAB_ORDER_UPDATED events. Target object| Context object "id": laborder_id "type": [LabOrder](/sdk/data-labs/#laborder) | "patient": "id": pt_id LAB_ORDER_UPDATED --- Occurs when a lab order is updated. Target object| Context object "id": laborder_id "type": [LabOrder](/sdk/data-labs/#laborder) | "patient": "id": pt_id LAB_REPORT_CREATED --- Occurs when a lab report is created either through Data Integration, electronic ingestion or the FHIR API. Target object| Context object "id": labreport_id "type": [LabReport](/sdk/data-labs/#labreport) | "patient": "id": pt_id LAB_REPORT_UPDATED --- Occurs when a lab report is updated. Target object| Context object "id": labreport_id "type": [LabReport](/sdk/data-labs/#labreport) | "patient": "id": pt_id #### Medications MEDICATION_LIST_ITEM_CREATED --- Occurs when a medication is added for a patient. Target object| Context object "id": medication_id "type": [Medication](/sdk/data-medication/#medication) | "patient": "id": pt_id MEDICATION_LIST_ITEM_UPDATED --- Occurs when a medication is updated for a patient. Target object| Context object "id": medication_id "type": [Medication](/sdk/data-medication/#medication) | "patient": "id": pt_id PRESCRIPTION_UPDATED --- Occurs when a prescription is updated. Target object| Context object "id": prescription_id "type": [Prescription](/sdk/data-prescription) | "patient": "id": pt_id PRESCRIPTION_CREATED --- Occurs when a prescription is created for a patient using the Prescribe command. Additional details for the prescription become available with subsequent PRESCRIPTION_UPDATED events. Target object| Context object "id": prescription_id "type": [Prescription](/sdk/data-prescription) | "patient": "id": pt_id ##### Prescription status events The following events fire when a prescription's status changes during the e-prescribing lifecycle. These events always fire alongside a `PRESCRIPTION_CREATED` or `PRESCRIPTION_UPDATED` event. For example, when a prescription is first created, both `PRESCRIPTION_CREATED` and `PRESCRIPTION_OPENED` will fire. When a prescription's status is updated to "transmitted", both `PRESCRIPTION_UPDATED` and `PRESCRIPTION_TRANSMITTED` will fire. PRESCRIPTION_OPENED --- Occurs when a prescription's status is set to open. This is the default status when a prescription is first created. Target object| Context object "id": prescription_id "type": [Prescription](/sdk/data-prescription) | "patient": "id": pt_id PRESCRIPTION_PENDING --- Occurs when a prescription's status changes to pending. Target object| Context object "id": prescription_id "type": [Prescription](/sdk/data-prescription) | "patient": "id": pt_id PRESCRIPTION_ACCEPTED --- Occurs when a prescription has been ultimately accepted. Target object| Context object "id": prescription_id "type": [Prescription](/sdk/data-prescription) | "patient": "id": pt_id PRESCRIPTION_ERRORED --- Occurs when an error occurs during prescription processing. Target object| Context object "id": prescription_id "type": [Prescription](/sdk/data-prescription) | "patient": "id": pt_id PRESCRIPTION_CANCEL_REQUESTED --- Occurs when a cancellation has been requested for a prescription. Target object| Context object "id": prescription_id "type": [Prescription](/sdk/data-prescription) | "patient": "id": pt_id PRESCRIPTION_CANCELED --- Occurs when a prescription has been successfully canceled. Target object| Context object "id": prescription_id "type": [Prescription](/sdk/data-prescription) | "patient": "id": pt_id PRESCRIPTION_CANCEL_DENIED --- Occurs when a cancellation request for a prescription has been denied. Target object| Context object "id": prescription_id "type": [Prescription](/sdk/data-prescription) | "patient": "id": pt_id PRESCRIPTION_RECEIVED --- Occurs when a prescription has been received by the e-prescribing network. Target object| Context object "id": prescription_id "type": [Prescription](/sdk/data-prescription) | "patient": "id": pt_id PRESCRIPTION_SIGNED --- Occurs when a prescription has been signed. Target object| Context object "id": prescription_id "type": [Prescription](/sdk/data-prescription) | "patient": "id": pt_id PRESCRIPTION_INQUEUE --- Occurs when a prescription is in queue for transmission. Target object| Context object "id": prescription_id "type": [Prescription](/sdk/data-prescription) | "patient": "id": pt_id PRESCRIPTION_TRANSMITTED --- Occurs when a prescription has been transmitted to the pharmacy. Target object| Context object "id": prescription_id "type": [Prescription](/sdk/data-prescription) | "patient": "id": pt_id PRESCRIPTION_DELIVERED --- Occurs when a prescription has been delivered to the pharmacy. Target object| Context object "id": prescription_id "type": [Prescription](/sdk/data-prescription) | "patient": "id": pt_id #### Surescripts Surescripts response events fire when the platform receives a response from Surescripts after a corresponding request effect is executed. These events let plugins react to insurance eligibility checks and other Surescripts services. SURESCRIPTS_ELIGIBILITY_RESPONSE --- Occurs when Surescripts returns an eligibility response after a `SendSurescriptsEligibilityRequestEffect` is executed. The response contains the patient's insurance plan information and coverage details. See [Handling Eligibility Responses](/sdk/effect-surescripts/#handling-eligibility-responses) for the typed response data classes and an example handler. Target object| Context object empty | "correlation_id": str "patient_id": str "plans": list[dict] "error": str or None SURESCRIPTS_BENEFITS_RESPONSE --- Occurs when Surescripts returns a benefits response after a `SendSurescriptsBenefitsRequestEffect` is executed. The response contains formulary and coverage details for the requested medication, including copays, quantity limits, and therapeutic alternatives. See [Handling Benefits Responses](/sdk/effect-surescripts/#handling-benefits-responses) for the typed response data classes and an example handler. Target object| Context object empty | "correlation_id": str "patient_id": str "medication_ndc": str "coverages": list[dict] "error": str or None #### Messaging MESSAGE_CREATED --- Occurs when a message (patient/practitioner communication) is created. Target object| Context object "id": message_id "type": [Message](/sdk/data-message/#message) | "patient": "id": pt_id MESSAGE_TRANSMISSION_CREATED --- Occurs when a message transmission record is created. Message transmissions track delivery attempts and status for messages sent through various channels (SMS, email, etc.). Target object| Context object "id": message_transmission_id "type": [MessageTransmission](/sdk/data-message/#messagetransmission) | empty MESSAGE_TRANSMISSION_UPDATED --- Occurs when a message transmission record is updated (e.g., when delivery status changes). Target object| Context object "id": message_transmission_id "type": [MessageTransmission](/sdk/data-message/#messagetransmission) | empty #### Notes NOTE_STATE_CHANGE_EVENT_CREATED --- Occurs as a note traverses through its state machine. This event can be used when looking at any changes to the [note state](/sdk/data-note/#notestates), including locking and unlocking. Target object| Context object "id": nsce_id "type": NoteStateChangeEvent | "note_id": note_id, "patient_id": pt_id, "state": [str](/sdk/data-note/#notestates) NOTE_STATE_CHANGE_EVENT_PRE_CREATE --- Occurs **before** a note state change event is created. This event allows protocols to perform validation and block the note state change if needed. If an [`EventValidationError`](/sdk/effect-event-validation-error) effect is returned, the note state change event is aborted and the error message is surfaced to the user. Target object| Context object "id": nsce_id "type": NoteStateChangeEvent | "note_id": note_id, "patient_id": pt_id, "state": [str](/sdk/data-note/#notestates) NOTE_STATE_CHANGE_EVENT_UPDATED --- Occurs if a note state change event is updated. Locking and unlocking both trigger an update event, and there is an *additional* update event when an archived PDF copy of the note finishes generating; this is done asynchronously. Target object| Context object "id": nsce_id "type": NoteStateChangeEvent | "note_id": note_id, "patient_id": pt_id, "state": [str](/sdk/data-note/#notestates) NOTE_CREATED --- Occurs when a note is created. Target object| Context object "id": note_id "type": [Note](/sdk/data-note/) | "patient": "id": pt_id NOTE_UPDATED --- Occurs when a note is updated, including changes to fields, commands, or body content. Target object| Context object "id": note_id "type": [Note](/sdk/data-note/) | "patient": "id": pt_id "user": "id": staff_key NOTE_OPENED --- Fires when a provider expands a note in the patient chart. The context includes the note's ID. Target object| Context object "id": patient_key "type": Patient | "note": {"id": note_uuid}, "user": { "type": str, "id": user_id } NOTE_CLOSED --- Fires when a provider collapses a note that was previously open. The context includes the note's ID. Target object| Context object "id": patient_key "type": Patient | "note": {"id": note_uuid}, "user": { "type": str, "id": user_id } NOTE_SUPERVISING_PROVIDER_CHANGED --- Occurs when a note's supervising provider is changed. The context includes the previous supervising provider's Staff ID, or `null` if the note previously had no supervising provider. Target object| Context object "id": note_id "type": [Note](/sdk/data-note/) | "previous": null | { "id": staff_key } GET_NOTE_RESTRICTIONS --- Fires every time a note is opened or its restrictions are refetched. Plugins respond with a [`NoteRestrictionsEffect`](/sdk/effect-note-restrictions/) to control whether the user can edit the note, whether the content is blurred, and what banner message is displayed. If no plugin responds, the note is unrestricted by default. Target object| Context object "id": note.id "type": [Note](/sdk/data-note/) | empty #### Letters LETTER_CREATED --- Occurs when a letter is created. Target object| Context object "id": letter_id "type": [Letter](/sdk/data-letter/) | "patient": "id": pt_id LETTER_UPDATED --- Occurs when a letter is updated. Target object| Context object "id": letter_id "type": [Letter](/sdk/data-letter/) | "patient": "id": pt_id LETTER_ACTION_EVENT_CREATED --- Occurs when a letter action event is created. Target object| Context object "id": letter_action_event_id "type": [LetterActionEvent](/sdk/data-letter-action-event/) | empty LETTER_ACTION_EVENT_UPDATED --- Occurs when a letter action event is updated. Target object| Context object "id": letter_action_event_id "type": [LetterActionEvent](/sdk/data-letter-action-event/) | empty #### Observations OBSERVATION_CREATED --- Occurs when an observation is created. Target object| Context object "id": observation_id "type": [Observation](/sdk/data-observation/#observation) | "patient": "id": pt_id OBSERVATION_UPDATED --- Occurs when an observation is updated. Target object| Context object "id": observation_id "type": [Observation](/sdk/data-observation/#observation) | "patient": "id": pt_id #### Protocol Overrides PROTOCOL_OVERRIDE_CREATED --- Target object| Context object "id": protocoloverride_id "type": [ProtocolOverride](/sdk/data-protocol-override/#protocoloverride) | "patient": "id": pt_id PROTOCOL_OVERRIDE_UPDATED --- Target object| Context object "id": protocoloverride_id "type": [ProtocolOverride](/sdk/data-protocol-override/#protocoloverride) | "patient": "id": pt_id PROTOCOL_OVERRIDE_DELETED --- Target object| Context object "id": protocoloverride_id "type": [ProtocolOverride](/sdk/data-protocol-override/#protocoloverride) | "patient": "id": pt_id #### Referral Reports REFERRAL_REPORT_CREATED --- Occurs when a specialist consult report is created in Data Integration. Target object| Context object "id": referralreport_id "type": [ReferralReport](/sdk/data-referral/#referralreport) | "patient": "id": pt_id REFERRAL_REPORT_UPDATED --- Occurs when a specialist consult report is updated. Target object| Context object "id": referralreport_id "type": [ReferralReport](/sdk/data-referral/#referralreport) | "patient": "id": pt_id #### Tasks TASK_CREATED --- Occurs when a task is created. Target object| Context object "id": task_id "type": [Task](/sdk/data-task/#task) | "patient": "id": pt_id TASK_UPDATED --- Occurs when a task is updated. Target object| Context object "id": task_id "type": [Task](/sdk/data-task/#task) | "patient": "id": pt_id TASK_COMMENT_CREATED --- Occurs when a comment is added to a task. Target object| Context object "id": taskcomment_id "type": [TaskComment](/sdk/data-task/#taskcomment) | empty TASK_COMMENT_UPDATED --- Occurs when a comment for a task is updated. Target object| Context object "id": taskcomment_id "type": [TaskComment](/sdk/data-task/#taskcomment) | empty TASK_COMMENT_DELETED --- Occurs when a comment for a task is removed. Target object| Context object "id": taskcomment_id "type": [TaskComment](/sdk/data-task/#taskcomment) | empty TASK_LABELS_ADJUSTED --- Occurs when a label is added to or removed from a task. **Note:** unlike the other `TASK_*` events, the target of this event is the `TaskLabel` that changed — _not_ the task. The affected task's ID is available in the context object as `task.id` (use that to load the task, e.g. `Task.objects.get(id=self.event.context["task"]["id"])`), and `action` tells you whether the label was `add`ed or `remove`d. Target object| Context object "id": task_label_id "type": [TaskLabel](/sdk/data-task/#tasklabel) | "patient": "id": pt_id "task": "id": task_id "action": literal["add", "remove"] TASK_COMPLETED --- Occurs when a task is set to completed. Target object| Context object "id": task_id "type": [Task](/sdk/data-task/#task) | "patient": "id": pt_id TASK_CLOSED --- Occurs when a task is set to closed. Target object| Context object "id": task_id "type": [Task](/sdk/data-task/#task) | "patient": "id": pt_id #### Staff STAFF_CREATED --- Occurs when a staff is created. Target object| Context object "id": staff_id "type": [Staff](/sdk/data-staff/#staff) | empty STAFF_UPDATED --- Occurs when a staff is updated. Target object| Context object "id": staff_id "type": [Staff](/sdk/data-staff/#staff) | empty STAFF_ACTIVATED --- Occurs when a staff record is created with active=True, or a staff record's active field is updated from False to True. Target object| Context object "id": staff_id "type": [Staff](/sdk/data-staff/#staff) | empty STAFF_DEACTIVATED --- Occurs when a staff record's active field is updated from True to False. Target object| Context object "id": staff_id "type": [Staff](/sdk/data-staff/#staff) | empty #### Staff External Identifier STAFF_EXTERNAL_IDENTIFIER_CREATED --- Occurs when an external identifier is created for a staff member. Target object| Context object "id": staffexternalidentifier_id "type": [StaffExternalIdentifier](/sdk/data-staff/#staffexternalidentifier) | "staff": "id": staff_id STAFF_EXTERNAL_IDENTIFIER_UPDATED --- Occurs when an external identifier for a staff member is updated. Target object| Context object "id": staffexternalidentifier_id "type": [StaffExternalIdentifier](/sdk/data-staff/#staffexternalidentifier) | "staff": "id": staff_id STAFF_EXTERNAL_IDENTIFIER_DELETED --- Occurs when an external identifier for a staff member is deleted. Target object| Context object "id": staffexternalidentifier_id "type": [StaffExternalIdentifier](/sdk/data-staff/#staffexternalidentifier) | "staff": "id": staff_id #### Staff Metadata STAFF_METADATA_CREATED --- Occurs when a staff member's metadata is created. Target object| Context object "id": staffmetadata_id "type": [StaffMetadata](/sdk/data-staff/#staffmetadata) | "staff": "id": staff_id STAFF_METADATA_UPDATED --- Occurs when a staff member's metadata is updated. Target object| Context object "id": staffmetadata_id "type": [StaffMetadata](/sdk/data-staff/#staffmetadata) | "staff": "id": staff_id STAFF_METADATA_DELETED --- Occurs when a staff member's metadata is deleted. Target object| Context object "id": staffmetadata_id "type": [StaffMetadata](/sdk/data-staff/#staffmetadata) | "staff": "id": staff_id #### Vital Signs VITAL_SIGN_CREATED --- Occurs when a vitals entry is created for a patient using the vitals command. Additional details for the vitals become available with subsequent VITAL_SIGN_UPDATED events. Target object| Context object "id": vitalsign_id "type": None | empty VITAL_SIGN_UPDATED --- Occurs when a vitals entry is updated for a patient. Target object| Context object "id": vitalsign_id "type": None | empty ### Command lifecycle events These events fire during the command lifecycle. #### Generic events Event| Occurs when| PRE_COMMAND_ORIGINATE| Before any command is entered into a note. ---|--- POST_COMMAND_ORIGINATE| After any command is entered into a note. PRE_COMMAND_UPDATE| Before the data in any command is updated. POST_COMMAND_UPDATE| After the data in any command is updated. PRE_COMMAND_COMMIT| Before any command is committed. POST_COMMAND_COMMIT| After any command is committed. PRE_COMMAND_DELETE| Before any command is deleted. POST_COMMAND_DELETE| After any command is deleted. PRE_COMMAND_ENTER_IN_ERROR| Before any command is marked as entered in error. POST_COMMAND_ENTER_IN_ERROR| After any command is marked as entered in error. PRE_COMMAND_EXECUTE_ACTION| Before an action is executed on any command. POST_COMMAND_EXECUTE_ACTION| After an action is executed on any command. POST_COMMAND_INSERTED_INTO_NOTE| After a command is added to a note in the UI. AVAILABLE_ACTIONS| When a command is rendered in the UI, after any update to data, state, or other changes ##### Transaction Behavior Pre-event handlers (`PRE_COMMAND_ORIGINATE`, `PRE_COMMAND_COMMIT`, `PRE_COMMAND_UPDATE`) run synchronously inside the same database transaction as the command operation. Your handler can perform validation or modify data, and if it raises an exception, both your changes and the command operation roll back together. Post-event handlers (`POST_COMMAND_ORIGINATE`, `POST_COMMAND_COMMIT`, `POST_COMMAND_UPDATE`) use Django's `on_commit` mechanism and execute only after the outermost transaction commits successfully. If you wrap a command operation inside a `transaction.atomic()` block, the post-event handlers won't fire until that outer transaction commits. This model lets you combine command operations with other database writes in a single atomic unit. You can originate a command and update related records together, knowing that either all operations succeed or none do. See [Transactions](/sdk/custom-data-transactions/) for more on using `transaction.atomic()` in your plugins. ##### Context Overview Each command lifecycle event provides specific context to the handler, depending on the stage of the command lifecycle. **Base Context (All Events Except`PRE_COMMAND_ORIGINATE`)**: ```json { "note": { "uuid": "note-123" }, "patient": { "id": "patient-123" }, "fields": { "key": "value" } } ``` - `note.uuid`: The unique identifier of the note associated with the command. - `patient.id`: The unique identifier of the patient associated with the note. - `fields`: A dictionary containing command-specific details. See examples for each command. **`PRE_COMMAND_ORIGINATE` Context**: Since the command is not yet connected to a note, the `PRE_COMMAND_ORIGINATE` event context only includes: ```json { "fields": { "key": "value" } } ``` - `fields`: Contains details specific to the command being originated. * * * #### Adjust Prescription Command ADJUST_PRESCRIPTION_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescribe": dict "change_medication_to": dict "indications": list[dict] "sig": str "days_supply": int "quantity_to_dispense": int "type_to_dispense": dict "refills": int "substitutions": str "pharmacy": dict "prescriber": dict "note_to_pharmacist": str "note": "uuid": note_id "patient": "id": pt_id ADJUST_PRESCRIPTION_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescribe": dict "change_medication_to": dict "indications": list[dict] "sig": str "days_supply": int "quantity_to_dispense": int "type_to_dispense": dict "refills": int "substitutions": str "pharmacy": dict "prescriber": dict "note_to_pharmacist": str "note": "uuid": note_id "patient": "id": pt_id ADJUST_PRESCRIPTION_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescribe": dict "change_medication_to": dict "indications": list[dict] "sig": str "days_supply": int "quantity_to_dispense": int "type_to_dispense": dict "refills": int "substitutions": str "pharmacy": dict "prescriber": dict "note_to_pharmacist": str "note": "uuid": note_id "patient": "id": pt_id ADJUST_PRESCRIPTION_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id ADJUST_PRESCRIPTION_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescribe": dict "change_medication_to": dict "indications": list[dict] "sig": str "days_supply": int "quantity_to_dispense": int "type_to_dispense": dict "refills": int "substitutions": str "pharmacy": dict "prescriber": dict "note_to_pharmacist": str "note": "uuid": note_id "patient": "id": pt_id ADJUST_PRESCRIPTION_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescribe": dict "change_medication_to": dict "indications": list[dict] "sig": str "days_supply": int "quantity_to_dispense": int "type_to_dispense": dict "refills": int "substitutions": str "pharmacy": dict "prescriber": dict "note_to_pharmacist": str "note": "uuid": note_id "patient": "id": pt_id ADJUST_PRESCRIPTION_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescribe": dict "change_medication_to": dict "indications": list[dict] "sig": str "days_supply": int "quantity_to_dispense": int "type_to_dispense": dict "refills": int "substitutions": str "pharmacy": dict "prescriber": dict "note_to_pharmacist": str "note": "uuid": note_id "patient": "id": pt_id ADJUST_PRESCRIPTION_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescribe": dict "change_medication_to": dict "indications": list[dict] "sig": str "days_supply": int "quantity_to_dispense": int "type_to_dispense": dict "refills": int "substitutions": str "pharmacy": dict "prescriber": dict "note_to_pharmacist": str "note": "uuid": note_id "patient": "id": pt_id ADJUST_PRESCRIPTION_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescribe": dict "change_medication_to": dict "indications": list[dict] "sig": str "days_supply": int "quantity_to_dispense": int "type_to_dispense": dict "refills": int "substitutions": str "pharmacy": dict "prescriber": dict "note_to_pharmacist": str "note": "uuid": note_id "patient": "id": pt_id ADJUST_PRESCRIPTION_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescribe": dict "indications": list[dict] "sig": str "days_supply": int "quantity_to_dispense": int "type_to_dispense": dict "refills": int "substitutions": str "pharmacy": dict "prescriber": dict "note_to_pharmacist": str "note": "uuid": note_id "patient": "id": pt_id ADJUST_PRESCRIPTION_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescribe": dict "change_medication_to": dict "indications": list[dict] "sig": str "days_supply": int "quantity_to_dispense": int "type_to_dispense": dict "refills": int "substitutions": str "pharmacy": dict "prescriber": dict "note_to_pharmacist": str "note": "uuid": note_id "patient": "id": pt_id ADJUST_PRESCRIPTION_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescribe": dict "change_medication_to": dict "indications": list[dict] "sig": str "days_supply": int "quantity_to_dispense": int "type_to_dispense": dict "refills": int "substitutions": str "pharmacy": dict "prescriber": dict "note_to_pharmacist": str "note": "uuid": note_id "patient": "id": pt_id ADJUST_PRESCRIPTION_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescribe": dict "change_medication_to": dict "indications": list[dict] "sig": str "days_supply": int "quantity_to_dispense": int "type_to_dispense": dict "refills": int "substitutions": str "pharmacy": dict "prescriber": dict "note_to_pharmacist": str "note": "uuid": note_id "patient": "id": pt_id ADJUST_PRESCRIPTION_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescribe": dict "change_medication_to": dict "indications": list[dict] "sig": str "days_supply": int "quantity_to_dispense": int "type_to_dispense": dict "refills": int "substitutions": str "pharmacy": dict "prescriber": dict "note_to_pharmacist": str "note": "uuid": note_id "patient": "id": pt_id ADJUST_PRESCRIPTION_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescribe": dict "change_medication_to": dict "indications": list[dict] "sig": str "days_supply": int "quantity_to_dispense": int "type_to_dispense": dict "refills": int "substitutions": str "pharmacy": dict "prescriber": dict "note_to_pharmacist": str "note": "uuid": note_id "patient": "id": pt_id ADJUST_PRESCRIPTION__INDICATIONS__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] ADJUST_PRESCRIPTION__INDICATIONS__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] ADJUST_PRESCRIPTION__PHARMACY__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] ADJUST_PRESCRIPTION__PHARMACY__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] ADJUST_PRESCRIPTION__PRESCRIBE__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[MedicationSearchResult] ADJUST_PRESCRIPTION__PRESCRIBE__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] ADJUST_PRESCRIPTION__CHANGE_MEDICATION_TO__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[MedicationSearchResult] ADJUST_PRESCRIPTION__CHANGE_MEDICATION_TO__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] ADJUST_PRESCRIPTION__SUPERVISING_PROVIDER__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] ADJUST_PRESCRIPTION__SUPERVISING_PROVIDER__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] ADJUST_PRESCRIPTION__PRESCRIBER__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] ADJUST_PRESCRIPTION__PRESCRIBER__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Allergy Command ALLERGY_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "allergy": dict "severity": str "narrative": str "approximate_date": "input": str "date": str "note": "uuid": note_id "patient": "id": pt_id ALLERGY_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "allergy": dict "severity": str "narrative": str "approximate_date": "input": str "date": str "note": "uuid": note_id "patient": "id": pt_id ALLERGY_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "allergy": dict "severity": str "narrative": str "approximate_date": "input": str "date": str "note": "uuid": note_id "patient": "id": pt_id ALLERGY_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id ALLERGY_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "allergy": dict "severity": str "narrative": str "approximate_date": "input": str "date": str "note": "uuid": note_id "patient": "id": pt_id ALLERGY_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "allergy": dict "severity": str "narrative": str "approximate_date": "input": str "date": str "note": "uuid": note_id "patient": "id": pt_id ALLERGY_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "allergy": dict "severity": str "narrative": str "approximate_date": "input": str "date": str "note": "uuid": note_id "patient": "id": pt_id ALLERGY_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "allergy": dict "severity": str "narrative": str "approximate_date": "input": str "date": str "note": "uuid": note_id "patient": "id": pt_id ALLERGY_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "allergy": dict "severity": str "narrative": str "approximate_date": "input": str "date": str "note": "uuid": note_id "patient": "id": pt_id ALLERGY_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "allergy": dict "severity": str "narrative": str "approximate_date": "input": str "date": str "note": "uuid": note_id "patient": "id": pt_id ALLERGY_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "allergy": dict "severity": str "narrative": str "approximate_date": "input": str "date": str "note": "uuid": note_id "patient": "id": pt_id ALLERGY_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "allergy": dict "severity": str "narrative": str "approximate_date": "input": str "date": str "note": "uuid": note_id "patient": "id": pt_id ALLERGY_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "allergy": dict "severity": str "narrative": str "approximate_date": "input": str "date": str "note": "uuid": note_id "patient": "id": pt_id ALLERGY_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "allergy": dict "severity": str "narrative": str "approximate_date": "input": str "date": str "note": "uuid": note_id "patient": "id": pt_id ALLERGY_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "allergy": dict "severity": str "narrative": str "approximate_date": "input": str "date": str "note": "uuid": note_id "patient": "id": pt_id ALLERGY__ALLERGY__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[AllergySearchResult] ALLERGY__ALLERGY__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Assess Command ASSESS_COMMAND__CONDITION_SELECTED --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "condition": dict "background": str "status": str "narrative": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "condition": dict "background": str "status": str "narrative": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "condition": dict "background": str "status": str "narrative": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "condition": dict "background": str "status": str "narrative": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id ASSESS_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "condition": dict "background": str "status": str "narrative": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "condition": dict "background": str "status": str "narrative": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "condition": dict "background": str "status": str "narrative": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "condition": dict "background": str "status": str "narrative": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "condition": dict "background": str "status": str "narrative": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "condition": dict "background": str "status": str "narrative": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "condition": dict "background": str "status": str "narrative": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "condition": dict "background": str "status": str "narrative": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "condition": dict "background": str "status": str "narrative": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "condition": dict "background": str "status": str "narrative": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "condition": dict "background": str "status": str "narrative": str "note": "uuid": note_id "patient": "id": pt_id ASSESS__CONDITION__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[ConditionSearchResult] ASSESS__CONDITION__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[ConditionSearchResult] #### Cancel Prescription Command CANCEL_PRESCRIPTION_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescription": str "note": "uuid": note_id "patient": "id": pt_id CANCEL_PRESCRIPTION_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescription": str "note": "uuid": note_id "patient": "id": pt_id CANCEL_PRESCRIPTION_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescription": str "note": "uuid": note_id "patient": "id": pt_id CANCEL_PRESCRIPTION_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescription": str "note": "uuid": note_id "patient": "id": pt_id CANCEL_PRESCRIPTION_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescription": str "note": "uuid": note_id "patient": "id": pt_id CANCEL_PRESCRIPTION_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescription": str "note": "uuid": note_id "patient": "id": pt_id CANCEL_PRESCRIPTION_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescription": str "note": "uuid": note_id "patient": "id": pt_id CANCEL_PRESCRIPTION_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescription": str "note": "uuid": note_id "patient": "id": pt_id CANCEL_PRESCRIPTION_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescription": str "note": "uuid": note_id "patient": "id": pt_id CANCEL_PRESCRIPTION_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescription": str "note": "uuid": note_id "patient": "id": pt_id CANCEL_PRESCRIPTION_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescription": str "note": "uuid": note_id "patient": "id": pt_id CANCEL_PRESCRIPTION_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescription": str "note": "uuid": note_id "patient": "id": pt_id CANCEL_PRESCRIPTION_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescription": str "note": "uuid": note_id "patient": "id": pt_id CANCEL_PRESCRIPTION_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "prescription": str "note": "uuid": note_id "patient": "id": pt_id CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] CANCEL_PRESCRIPTION__SELECTED_PRESCRIPTION__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Change Medication Command CHANGE_MEDICATION_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id CHANGE_MEDICATION_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id CHANGE_MEDICATION_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id CHANGE_MEDICATION_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id CHANGE_MEDICATION_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id CHANGE_MEDICATION_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id CHANGE_MEDICATION_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id CHANGE_MEDICATION_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id CHANGE_MEDICATION_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id CHANGE_MEDICATION_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id CHANGE_MEDICATION_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id CHANGE_MEDICATION_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id CHANGE_MEDICATION_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id CHANGE_MEDICATION_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id CHANGE_MEDICATION__MEDICATION__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[MedicationSearchResult] CHANGE_MEDICATION__MEDICATION__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Chart Section Review Command CHART_SECTION_REVIEW_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "section": str "note": "uuid": note_id "patient": "id": pt_id CHART_SECTION_REVIEW_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "section": str "note": "uuid": note_id "patient": "id": pt_id CHART_SECTION_REVIEW_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "section": str "note": "uuid": note_id "patient": "id": pt_id CHART_SECTION_REVIEW_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "section": str "note": "uuid": note_id "patient": "id": pt_id CHART_SECTION_REVIEW_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id CHART_SECTION_REVIEW_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "section": str "note": "uuid": note_id "patient": "id": pt_id CHART_SECTION_REVIEW_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "section": str "note": "uuid": note_id "patient": "id": pt_id CHART_SECTION_REVIEW_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "section": str "note": "uuid": note_id "patient": "id": pt_id #### Clipboard Command CLIPBOARD_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "text": str "note": "uuid": note_id "patient": "id": pt_id CLIPBOARD_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "text": str "note": "uuid": note_id "patient": "id": pt_id CLIPBOARD_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "text": str "note": "uuid": note_id "patient": "id": pt_id CLIPBOARD_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id CLIPBOARD_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "text": str "note": "uuid": note_id "patient": "id": pt_id CLIPBOARD_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "text": str "note": "uuid": note_id "patient": "id": pt_id CLIPBOARD_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "text": str "note": "uuid": note_id "patient": "id": pt_id CLIPBOARD_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "text": str "note": "uuid": note_id "patient": "id": pt_id CLIPBOARD_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "text": str "note": "uuid": note_id "patient": "id": pt_id CLIPBOARD_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "text": str "note": "uuid": note_id "patient": "id": pt_id CLIPBOARD_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "text": str "note": "uuid": note_id "patient": "id": pt_id CLIPBOARD_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "text": str "note": "uuid": note_id "patient": "id": pt_id CLIPBOARD_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "text": str "note": "uuid": note_id "patient": "id": pt_id CLIPBOARD_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "text": str "note": "uuid": note_id "patient": "id": pt_id CLIPBOARD_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "text": str "note": "uuid": note_id "patient": "id": pt_id ##### Clipboard Fields Context The Clipboard Command provides the following fields in its context: Field| Type| Description ---|---|--- `text`| _string_| The raw text content copied to the clipboard. Refer to the base context documentation for additional details about the full context structure. ```json { "note": { "uuid": "note-123" }, "patient": { "id": "patient-123" }, "fields": { "text": "Patient complains of persistent headaches for the past two weeks." } } ``` * * * #### Close Goal Command CLOSE_GOAL_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_id": dict "achievement_status": str "progress": str "note": "uuid": note_id "patient": "id": pt_id CLOSE_GOAL_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_id": dict "achievement_status": str "progress": str "note": "uuid": note_id "patient": "id": pt_id CLOSE_GOAL_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_id": dict "achievement_status": str "progress": str "note": "uuid": note_id "patient": "id": pt_id CLOSE_GOAL_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id CLOSE_GOAL_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_id": dict "achievement_status": str "progress": str "note": "uuid": note_id "patient": "id": pt_id CLOSE_GOAL_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_id": dict "achievement_status": str "progress": str "note": "uuid": note_id "patient": "id": pt_id CLOSE_GOAL_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_id": dict "achievement_status": str "progress": str "note": "uuid": note_id "patient": "id": pt_id CLOSE_GOAL_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_id": dict "achievement_status": str "progress": str "note": "uuid": note_id "patient": "id": pt_id CLOSE_GOAL_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_id": dict "achievement_status": str "progress": str "note": "uuid": note_id "patient": "id": pt_id CLOSE_GOAL_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_id": dict "achievement_status": str "progress": str "note": "uuid": note_id "patient": "id": pt_id CLOSE_GOAL_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_id": dict "achievement_status": str "progress": str "note": "uuid": note_id "patient": "id": pt_id CLOSE_GOAL_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_id": dict "achievement_status": str "progress": str "note": "uuid": note_id "patient": "id": pt_id CLOSE_GOAL_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_id": dict "achievement_status": str "progress": str "note": "uuid": note_id "patient": "id": pt_id CLOSE_GOAL_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_id": dict "achievement_status": str "progress": str "note": "uuid": note_id "patient": "id": pt_id CLOSE_GOAL_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_id": dict "achievement_status": str "progress": str "note": "uuid": note_id "patient": "id": pt_id CLOSE_GOAL__GOAL_ID__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] CLOSE_GOAL__GOAL_ID__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Assess Coding Gap Command ASSESS_CODING_GAP_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "details": str "diagnose": list "background": str "approximate_date_of_onset": str "todays_assessment": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_CODING_GAP_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "details": str "diagnose": list "background": str "approximate_date_of_onset": str "todays_assessment": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_CODING_GAP_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "details": str "diagnose": list "background": str "approximate_date_of_onset": str "todays_assessment": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_CODING_GAP_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "details": str "diagnose": list "background": str "approximate_date_of_onset": str "todays_assessment": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_CODING_GAP_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "details": str "diagnose": list "background": str "approximate_date_of_onset": str "todays_assessment": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_CODING_GAP_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "details": str "diagnose": list "background": str "approximate_date_of_onset": str "todays_assessment": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_CODING_GAP_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "details": str "diagnose": list "background": str "approximate_date_of_onset": str "todays_assessment": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_CODING_GAP_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "details": str "diagnose": list "background": str "approximate_date_of_onset": str "todays_assessment": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_CODING_GAP_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "details": str "diagnose": list "background": str "approximate_date_of_onset": str "todays_assessment": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_CODING_GAP_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "details": str "diagnose": list "background": str "approximate_date_of_onset": str "todays_assessment": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_CODING_GAP_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "details": str "diagnose": list "background": str "approximate_date_of_onset": str "todays_assessment": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_CODING_GAP_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "details": str "diagnose": list "background": str "approximate_date_of_onset": str "todays_assessment": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_CODING_GAP_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "details": str "diagnose": list "background": str "approximate_date_of_onset": str "todays_assessment": str "note": "uuid": note_id "patient": "id": pt_id ASSESS_CODING_GAP_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "details": str "diagnose": list "background": str "approximate_date_of_onset": str "todays_assessment": str "note": "uuid": note_id "patient": "id": pt_id #### Create Coding Gap Command CREATE_CODING_GAP_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": list "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id CREATE_CODING_GAP_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": list "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id CREATE_CODING_GAP_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": list "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id CREATE_CODING_GAP_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": list "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id CREATE_CODING_GAP_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": list "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id CREATE_CODING_GAP_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": list "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id CREATE_CODING_GAP_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": list "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id CREATE_CODING_GAP_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": list "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id CREATE_CODING_GAP_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": list "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id CREATE_CODING_GAP_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": list "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id CREATE_CODING_GAP_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": list "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id CREATE_CODING_GAP_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": list "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id CREATE_CODING_GAP_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": list "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id CREATE_CODING_GAP_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": list "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id #### Defer Coding Gap Command DEFER_CODING_GAP_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "note": "uuid": note_id "patient": "id": pt_id DEFER_CODING_GAP_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "note": "uuid": note_id "patient": "id": pt_id DEFER_CODING_GAP_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "note": "uuid": note_id "patient": "id": pt_id DEFER_CODING_GAP_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "note": "uuid": note_id "patient": "id": pt_id DEFER_CODING_GAP_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "note": "uuid": note_id "patient": "id": pt_id DEFER_CODING_GAP_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "note": "uuid": note_id "patient": "id": pt_id DEFER_CODING_GAP_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "note": "uuid": note_id "patient": "id": pt_id DEFER_CODING_GAP_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "note": "uuid": note_id "patient": "id": pt_id DEFER_CODING_GAP_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "note": "uuid": note_id "patient": "id": pt_id DEFER_CODING_GAP_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "note": "uuid": note_id "patient": "id": pt_id DEFER_CODING_GAP_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "note": "uuid": note_id "patient": "id": pt_id DEFER_CODING_GAP_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "note": "uuid": note_id "patient": "id": pt_id DEFER_CODING_GAP_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "note": "uuid": note_id "patient": "id": pt_id DEFER_CODING_GAP_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "note": "uuid": note_id "patient": "id": pt_id #### Validate Coding Gap Command VALIDATE_CODING_GAP_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id VALIDATE_CODING_GAP_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id VALIDATE_CODING_GAP_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id VALIDATE_CODING_GAP_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id VALIDATE_CODING_GAP_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id VALIDATE_CODING_GAP_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id VALIDATE_CODING_GAP_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id VALIDATE_CODING_GAP_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id VALIDATE_CODING_GAP_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id VALIDATE_CODING_GAP_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id VALIDATE_CODING_GAP_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id VALIDATE_CODING_GAP_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id VALIDATE_CODING_GAP_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id VALIDATE_CODING_GAP_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "detected_issue": str "status": str "date": str "details": str "note": "uuid": note_id "patient": "id": pt_id #### Custom Command CUSTOM_COMMAND_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "content": str "schema_key": str "note": "uuid": note_id "patient": "id": pt_id CUSTOM_COMMAND_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "content": str "schema_key": str "note": "uuid": note_id "patient": "id": pt_id CUSTOM_COMMAND_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "content": str "schema_key": str "note": "uuid": note_id "patient": "id": pt_id CUSTOM_COMMAND_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "content": str "schema_key": str "note": "uuid": note_id "patient": "id": pt_id CUSTOM_COMMAND_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "content": str "schema_key": str "note": "uuid": note_id "patient": "id": pt_id CUSTOM_COMMAND_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "content": str "schema_key": str "note": "uuid": note_id "patient": "id": pt_id CUSTOM_COMMAND_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "content": str "schema_key": str "note": "uuid": note_id "patient": "id": pt_id CUSTOM_COMMAND_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "content": str "schema_key": str "note": "uuid": note_id "patient": "id": pt_id CUSTOM_COMMAND_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "content": str "schema_key": str "note": "uuid": note_id "patient": "id": pt_id CUSTOM_COMMAND_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "content": str "schema_key": str "note": "uuid": note_id "patient": "id": pt_id CUSTOM_COMMAND_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "content": str "schema_key": str "note": "uuid": note_id "patient": "id": pt_id CUSTOM_COMMAND_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "content": str "schema_key": str "note": "uuid": note_id "patient": "id": pt_id CUSTOM_COMMAND_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "content": str "schema_key": str "note": "uuid": note_id "patient": "id": pt_id CUSTOM_COMMAND_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": str "user": "staff": staff_id #### Diagnose Command DIAGNOSE_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": dict "background": str "approximate_date_of_onset": "input": str "date": str "today_assessment": str "note": "uuid": note_id "patient": "id": pt_id DIAGNOSE_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": dict "background": str "approximate_date_of_onset": "input": str "date": str "today_assessment": str "note": "uuid": note_id "patient": "id": pt_id DIAGNOSE_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": dict "background": str "approximate_date_of_onset": "input": str "date": str "today_assessment": str "note": "uuid": note_id "patient": "id": pt_id DIAGNOSE_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id DIAGNOSE_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": dict "background": str "approximate_date_of_onset": "input": str "date": str "today_assessment": str "note": "uuid": note_id "patient": "id": pt_id DIAGNOSE_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": dict "background": str "approximate_date_of_onset": "input": str "date": str "today_assessment": str "note": "uuid": note_id "patient": "id": pt_id DIAGNOSE_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": dict "background": str "approximate_date_of_onset": "input": str "date": str "today_assessment": str "note": "uuid": note_id "patient": "id": pt_id DIAGNOSE_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": dict "background": str "approximate_date_of_onset": "input": str "date": str "today_assessment": str "note": "uuid": note_id "patient": "id": pt_id DIAGNOSE_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": dict "background": str "approximate_date_of_onset": "input": str "date": str "today_assessment": str "note": "uuid": note_id "patient": "id": pt_id DIAGNOSE_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": dict "background": str "approximate_date_of_onset": "input": str "date": str "today_assessment": str "note": "uuid": note_id "patient": "id": pt_id DIAGNOSE_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": dict "background": str "approximate_date_of_onset": "input": str "date": str "today_assessment": str "note": "uuid": note_id "patient": "id": pt_id DIAGNOSE_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": dict "background": str "approximate_date_of_onset": "input": str "date": str "today_assessment": str "note": "uuid": note_id "patient": "id": pt_id DIAGNOSE_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": dict "background": str "approximate_date_of_onset": "input": str "date": str "today_assessment": str "note": "uuid": note_id "patient": "id": pt_id DIAGNOSE_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": dict "background": str "approximate_date_of_onset": "input": str "date": str "today_assessment": str "note": "uuid": note_id "patient": "id": pt_id DIAGNOSE_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "diagnose": dict "background": str "approximate_date_of_onset": "input": str "date": str "today_assessment": str "note": "uuid": note_id "patient": "id": pt_id DIAGNOSE__DIAGNOSE__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[ConditionSearchResult] DIAGNOSE__DIAGNOSE__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Educational Material Command EDUCATIONAL_MATERIAL_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": dict "note": "uuid": note_id "patient": "id": pt_id EDUCATIONAL_MATERIAL_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": dict "note": "uuid": note_id "patient": "id": pt_id EDUCATIONAL_MATERIAL_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": dict "note": "uuid": note_id "patient": "id": pt_id EDUCATIONAL_MATERIAL_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id EDUCATIONAL_MATERIAL_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": dict "note": "uuid": note_id "patient": "id": pt_id EDUCATIONAL_MATERIAL_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": dict "note": "uuid": note_id "patient": "id": pt_id EDUCATIONAL_MATERIAL_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": dict "note": "uuid": note_id "patient": "id": pt_id EDUCATIONAL_MATERIAL_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": dict "note": "uuid": note_id "patient": "id": pt_id EDUCATIONAL_MATERIAL_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": dict "note": "uuid": note_id "patient": "id": pt_id EDUCATIONAL_MATERIAL_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": dict "note": "uuid": note_id "patient": "id": pt_id EDUCATIONAL_MATERIAL_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": dict "note": "uuid": note_id "patient": "id": pt_id EDUCATIONAL_MATERIAL_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": dict "note": "uuid": note_id "patient": "id": pt_id EDUCATIONAL_MATERIAL_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": dict "note": "uuid": note_id "patient": "id": pt_id EDUCATIONAL_MATERIAL_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": dict "note": "uuid": note_id "patient": "id": pt_id EDUCATIONAL_MATERIAL_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": dict "note": "uuid": note_id "patient": "id": pt_id EDUCATIONAL_MATERIAL__LANGUAGE__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] EDUCATIONAL_MATERIAL__LANGUAGE__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] EDUCATIONAL_MATERIAL__TITLE__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] EDUCATIONAL_MATERIAL__TITLE__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Family History Command FAMILY_HISTORY_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "family_history": dict "relative": dict "note": str "note": "uuid": note_id "patient": "id": pt_id FAMILY_HISTORY_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "family_history": dict "relative": dict "note": str "note": "uuid": note_id "patient": "id": pt_id FAMILY_HISTORY_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "family_history": dict "relative": dict "note": str "note": "uuid": note_id "patient": "id": pt_id FAMILY_HISTORY_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id FAMILY_HISTORY_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "family_history": dict "relative": dict "note": str "note": "uuid": note_id "patient": "id": pt_id FAMILY_HISTORY_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "family_history": dict "relative": dict "note": str "note": "uuid": note_id "patient": "id": pt_id FAMILY_HISTORY_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "family_history": dict "relative": dict "note": str "note": "uuid": note_id "patient": "id": pt_id FAMILY_HISTORY_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "family_history": dict "relative": dict "note": str "note": "uuid": note_id "patient": "id": pt_id FAMILY_HISTORY_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "family_history": dict "relative": dict "note": str "note": "uuid": note_id "patient": "id": pt_id FAMILY_HISTORY_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "family_history": dict "relative": dict "note": str "note": "uuid": note_id "patient": "id": pt_id FAMILY_HISTORY_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "family_history": dict "relative": dict "note": str "note": "uuid": note_id "patient": "id": pt_id FAMILY_HISTORY_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "family_history": dict "relative": dict "note": str "note": "uuid": note_id "patient": "id": pt_id FAMILY_HISTORY_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "family_history": dict "relative": dict "note": str "note": "uuid": note_id "patient": "id": pt_id FAMILY_HISTORY_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "family_history": dict "relative": dict "note": str "note": "uuid": note_id "patient": "id": pt_id FAMILY_HISTORY_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "family_history": dict "relative": dict "note": str "note": "uuid": note_id "patient": "id": pt_id FAMILY_HISTORY__FAMILY_HISTORY__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] FAMILY_HISTORY__FAMILY_HISTORY__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] FAMILY_HISTORY__RELATIVE__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] FAMILY_HISTORY__RELATIVE__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Follow Up Command FOLLOW_UP_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "requested_date": dict "note_type": dict "coding": dict "reason_for_visit": dict "comment": str "note": "uuid": note_id "patient": "id": pt_id FOLLOW_UP_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "requested_date": dict "note_type": dict "coding": dict "reason_for_visit": dict "comment": str "note": "uuid": note_id "patient": "id": pt_id FOLLOW_UP_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "requested_date": dict "note_type": dict "coding": dict "reason_for_visit": dict "comment": str "note": "uuid": note_id "patient": "id": pt_id FOLLOW_UP_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id FOLLOW_UP_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "requested_date": dict "note_type": dict "coding": dict "reason_for_visit": dict "comment": str "note": "uuid": note_id "patient": "id": pt_id FOLLOW_UP_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "requested_date": dict "note_type": dict "coding": dict "reason_for_visit": dict "comment": str "note": "uuid": note_id "patient": "id": pt_id FOLLOW_UP_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "requested_date": dict "note_type": dict "coding": dict "reason_for_visit": dict "comment": str "note": "uuid": note_id "patient": "id": pt_id FOLLOW_UP_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "requested_date": dict "note_type": dict "coding": dict "reason_for_visit": dict "comment": str "note": "uuid": note_id "patient": "id": pt_id FOLLOW_UP_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "requested_date": dict "note_type": dict "coding": dict "reason_for_visit": dict "comment": str "note": "uuid": note_id "patient": "id": pt_id FOLLOW_UP_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "requested_date": dict "note_type": dict "coding": dict "reason_for_visit": dict "comment": str "note": "uuid": note_id "patient": "id": pt_id FOLLOW_UP_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "requested_date": dict "note_type": dict "coding": dict "reason_for_visit": dict "comment": str "note": "uuid": note_id "patient": "id": pt_id FOLLOW_UP_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "requested_date": dict "note_type": dict "coding": dict "reason_for_visit": dict "comment": str "note": "uuid": note_id "patient": "id": pt_id FOLLOW_UP_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "requested_date": dict "note_type": dict "coding": dict "reason_for_visit": dict "comment": str "note": "uuid": note_id "patient": "id": pt_id FOLLOW_UP_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "requested_date": dict "note_type": dict "coding": dict "reason_for_visit": dict "comment": str "note": "uuid": note_id "patient": "id": pt_id FOLLOW_UP_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "requested_date": dict "note_type": dict "coding": dict "reason_for_visit": dict "comment": str "note": "uuid": note_id "patient": "id": pt_id FOLLOW_UP__CODING__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] FOLLOW_UP__CODING__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] FOLLOW_UP__NOTE_TYPE__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] FOLLOW_UP__NOTE_TYPE__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Goal Command GOAL_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_statement": str "start_date": str "due_date": str "achievement_status": str "priority": str "progress": str "note": "uuid": note_id "patient": "id": pt_id GOAL_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_statement": str "start_date": str "due_date": str "achievement_status": str "priority": str "progress": str "note": "uuid": note_id "patient": "id": pt_id GOAL_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_statement": str "start_date": str "due_date": str "achievement_status": str "priority": str "progress": str "note": "uuid": note_id "patient": "id": pt_id GOAL_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id GOAL_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_statement": str "start_date": str "due_date": str "achievement_status": str "priority": str "progress": str "note": "uuid": note_id "patient": "id": pt_id GOAL_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_statement": str "start_date": str "due_date": str "achievement_status": str "priority": str "progress": str "note": "uuid": note_id "patient": "id": pt_id GOAL_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_statement": str "start_date": str "due_date": str "achievement_status": str "priority": str "progress": str "note": "uuid": note_id "patient": "id": pt_id GOAL_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_statement": str "start_date": str "due_date": str "achievement_status": str "priority": str "progress": str "note": "uuid": note_id "patient": "id": pt_id GOAL_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_statement": str "start_date": str "due_date": str "achievement_status": str "priority": str "progress": str "note": "uuid": note_id "patient": "id": pt_id GOAL_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_statement": str "start_date": str "due_date": str "achievement_status": str "priority": str "progress": str "note": "uuid": note_id "patient": "id": pt_id GOAL_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_statement": str "start_date": str "due_date": str "achievement_status": str "priority": str "progress": str "note": "uuid": note_id "patient": "id": pt_id GOAL_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_statement": str "start_date": str "due_date": str "achievement_status": str "priority": str "progress": str "note": "uuid": note_id "patient": "id": pt_id GOAL_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_statement": str "start_date": str "due_date": str "achievement_status": str "priority": str "progress": str "note": "uuid": note_id "patient": "id": pt_id GOAL_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_statement": str "start_date": str "due_date": str "achievement_status": str "priority": str "progress": str "note": "uuid": note_id "patient": "id": pt_id GOAL_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "goal_statement": str "start_date": str "due_date": str "achievement_status": str "priority": str "progress": str "note": "uuid": note_id "patient": "id": pt_id #### History of Present Illness Command HISTORY_OF_PRESENT_ILLNESS_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id HISTORY_OF_PRESENT_ILLNESS_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id HISTORY_OF_PRESENT_ILLNESS_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id HISTORY_OF_PRESENT_ILLNESS_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id HISTORY_OF_PRESENT_ILLNESS_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id HISTORY_OF_PRESENT_ILLNESS_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id HISTORY_OF_PRESENT_ILLNESS_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id HISTORY_OF_PRESENT_ILLNESS_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id HISTORY_OF_PRESENT_ILLNESS_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id HISTORY_OF_PRESENT_ILLNESS_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id HISTORY_OF_PRESENT_ILLNESS_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id HISTORY_OF_PRESENT_ILLNESS_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id HISTORY_OF_PRESENT_ILLNESS_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id HISTORY_OF_PRESENT_ILLNESS_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id HISTORY_OF_PRESENT_ILLNESS_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id #### Imaging Order Command IMAGING_ORDER_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "image": dict "indications": list[dict] "priority": str "additional_details": str "imaging_center": dict "comment": str "ordering_provider": dict "linked_items": list[dict] "note": "uuid": note_id "patient": "id": pt_id IMAGING_ORDER_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "image": dict "indications": list[dict] "priority": str "additional_details": str "imaging_center": dict "comment": str "ordering_provider": dict "linked_items": list[dict] "note": "uuid": note_id "patient": "id": pt_id IMAGING_ORDER_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "image": dict "indications": list[dict] "priority": str "additional_details": str "imaging_center": dict "comment": str "ordering_provider": dict "linked_items": list[dict] "note": "uuid": note_id "patient": "id": pt_id IMAGING_ORDER_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id IMAGING_ORDER_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "image": dict "indications": list[dict] "priority": str "additional_details": str "imaging_center": dict "comment": str "ordering_provider": dict "linked_items": list[dict] "note": "uuid": note_id "patient": "id": pt_id IMAGING_ORDER_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "image": dict "indications": list[dict] "priority": str "additional_details": str "imaging_center": dict "comment": str "ordering_provider": dict "linked_items": list[dict] "note": "uuid": note_id "patient": "id": pt_id IMAGING_ORDER_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "image": dict "indications": list[dict] "priority": str "additional_details": str "imaging_center": dict "comment": str "ordering_provider": dict "linked_items": list[dict] "note": "uuid": note_id "patient": "id": pt_id IMAGING_ORDER_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "image": dict "indications": list[dict] "priority": str "additional_details": str "imaging_center": dict "comment": str "ordering_provider": dict "linked_items": list[dict] "note": "uuid": note_id "patient": "id": pt_id IMAGING_ORDER_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "image": dict "indications": list[dict] "priority": str "additional_details": str "imaging_center": dict "comment": str "ordering_provider": dict "linked_items": list[dict] "note": "uuid": note_id "patient": "id": pt_id IMAGING_ORDER_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "image": dict "indications": list[dict] "priority": str "additional_details": str "imaging_center": dict "comment": str "ordering_provider": dict "linked_items": list[dict] "note": "uuid": note_id "patient": "id": pt_id IMAGING_ORDER_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "image": dict "indications": list[dict] "priority": str "additional_details": str "imaging_center": dict "comment": str "ordering_provider": dict "linked_items": list[dict] "note": "uuid": note_id "patient": "id": pt_id IMAGING_ORDER_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "image": dict "indications": list[dict] "priority": str "additional_details": str "imaging_center": dict "comment": str "ordering_provider": dict "linked_items": list[dict] "note": "uuid": note_id "patient": "id": pt_id IMAGING_ORDER_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "image": dict "indications": list[dict] "priority": str "additional_details": str "imaging_center": dict "comment": str "ordering_provider": dict "linked_items": list[dict] "note": "uuid": note_id "patient": "id": pt_id IMAGING_ORDER_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "image": dict "indications": list[dict] "priority": str "additional_details": str "imaging_center": dict "comment": str "ordering_provider": dict "linked_items": list[dict] "note": "uuid": note_id "patient": "id": pt_id IMAGING_ORDER_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "image": dict "indications": list[dict] "priority": str "additional_details": str "imaging_center": dict "comment": str "ordering_provider": dict "linked_items": list[dict] "note": "uuid": note_id "patient": "id": pt_id IMAGING_ORDER__IMAGE__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMAGING_ORDER__IMAGE__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMAGING_ORDER__IMAGING_CENTER__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMAGING_ORDER__IMAGING_CENTER__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMAGING_ORDER__INDICATIONS__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMAGING_ORDER__INDICATIONS__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMAGING_ORDER__ORDERING_PROVIDER__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMAGING_ORDER__ORDERING_PROVIDER__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Imaging Review Command IMAGING_REVIEW_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id IMAGING_REVIEW_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id IMAGING_REVIEW_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id IMAGING_REVIEW_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id IMAGING_REVIEW_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id IMAGING_REVIEW_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id IMAGING_REVIEW_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id IMAGING_REVIEW_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id IMAGING_REVIEW_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id IMAGING_REVIEW_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id IMAGING_REVIEW_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id IMAGING_REVIEW_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id IMAGING_REVIEW__REPORT__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMAGING_REVIEW__REPORT__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMAGING_REVIEW__COMMUNICATION_METHOD__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMAGING_REVIEW__COMMUNICATION_METHOD__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Immunization Statement Command IMMUNIZATION_STATEMENT_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "statement": dict "date": "date": str "input": str "comments": str "note": "uuid": note_id "patient": "id": pt_id IMMUNIZATION_STATEMENT_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "statement": dict "date": "date": str "input": str "comments": str "note": "uuid": note_id "patient": "id": pt_id IMMUNIZATION_STATEMENT_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "statement": dict "date": "date": str "input": str "comments": str "note": "uuid": note_id "patient": "id": pt_id IMMUNIZATION_STATEMENT_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "statement": dict "date": "date": str "input": str "comments": str "note": "uuid": note_id "patient": "id": pt_id IMMUNIZATION_STATEMENT_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "statement": dict "date": "date": str "input": str "comments": str "note": "uuid": note_id "patient": "id": pt_id IMMUNIZATION_STATEMENT_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "statement": dict "date": "date": str "input": str "comments": str "note": "uuid": note_id "patient": "id": pt_id IMMUNIZATION_STATEMENT_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "statement": dict "date": "date": str "input": str "comments": str "note": "uuid": note_id "patient": "id": pt_id IMMUNIZATION_STATEMENT_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "statement": dict "date": "date": str "input": str "comments": str "note": "uuid": note_id "patient": "id": pt_id IMMUNIZATION_STATEMENT_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "statement": dict "date": "date": str "input": str "comments": str "note": "uuid": note_id "patient": "id": pt_id IMMUNIZATION_STATEMENT_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "statement": dict "date": "date": str "input": str "comments": str "note": "uuid": note_id "patient": "id": pt_id IMMUNIZATION_STATEMENT_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "statement": dict "date": "date": str "input": str "comments": str "note": "uuid": note_id "patient": "id": pt_id IMMUNIZATION_STATEMENT_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "statement": dict "date": "date": str "input": str "comments": str "note": "uuid": note_id "patient": "id": pt_id IMMUNIZATION_STATEMENT_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "statement": dict "date": "date": str "input": str "comments": str "note": "uuid": note_id "patient": "id": pt_id IMMUNIZATION_STATEMENT_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "statement": dict "date": "date": str "input": str "comments": str "note": "uuid": note_id "patient": "id": pt_id IMMUNIZATION_STATEMENT__STATEMENT__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMMUNIZATION_STATEMENT__STATEMENT__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMMUNIZATION_STATEMENT_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id #### Immunize Command IMMUNIZE_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "coding": dict "lot_number": dict "manufacturer": str "exp_date_original": str "sig_original": str "consent_given": bool "given_by": dict "note": "uuid": note_id "patient": "id": pt_id IMMUNIZE_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "coding": dict "lot_number": dict "manufacturer": str "exp_date_original": str "sig_original": str "consent_given": bool "given_by": dict "note": "uuid": note_id "patient": "id": pt_id IMMUNIZE_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "coding": dict "lot_number": dict "manufacturer": str "exp_date_original": str "sig_original": str "consent_given": bool "given_by": dict "note": "uuid": note_id "patient": "id": pt_id IMMUNIZE_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id IMMUNIZE_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "coding": dict "lot_number": dict "manufacturer": str "exp_date_original": str "sig_original": str "consent_given": bool "given_by": dict "note": "uuid": note_id "patient": "id": pt_id IMMUNIZE_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "coding": dict "lot_number": dict "manufacturer": str "exp_date_original": str "sig_original": str "consent_given": bool "given_by": dict "note": "uuid": note_id "patient": "id": pt_id IMMUNIZE_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "coding": dict "lot_number": dict "manufacturer": str "exp_date_original": str "sig_original": str "consent_given": bool "given_by": dict "note": "uuid": note_id "patient": "id": pt_id IMMUNIZE_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "coding": dict "lot_number": dict "manufacturer": str "exp_date_original": str "sig_original": str "consent_given": bool "given_by": dict "note": "uuid": note_id "patient": "id": pt_id IMMUNIZE_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "coding": dict "lot_number": dict "manufacturer": str "exp_date_original": str "sig_original": str "consent_given": bool "given_by": dict "note": "uuid": note_id "patient": "id": pt_id IMMUNIZE_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "coding": dict "lot_number": dict "manufacturer": str "exp_date_original": str "sig_original": str "consent_given": bool "given_by": dict "note": "uuid": note_id "patient": "id": pt_id IMMUNIZE_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "coding": dict "lot_number": dict "manufacturer": str "exp_date_original": str "sig_original": str "consent_given": bool "given_by": dict "note": "uuid": note_id "patient": "id": pt_id IMMUNIZE_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "coding": dict "lot_number": dict "manufacturer": str "exp_date_original": str "sig_original": str "consent_given": bool "given_by": dict "note": "uuid": note_id "patient": "id": pt_id IMMUNIZE_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "coding": dict "lot_number": dict "manufacturer": str "exp_date_original": str "sig_original": str "consent_given": bool "given_by": dict "note": "uuid": note_id "patient": "id": pt_id IMMUNIZE_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "coding": dict "lot_number": dict "manufacturer": str "exp_date_original": str "sig_original": str "consent_given": bool "given_by": dict "note": "uuid": note_id "patient": "id": pt_id IMMUNIZE_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "coding": dict "lot_number": dict "manufacturer": str "exp_date_original": str "sig_original": str "consent_given": bool "given_by": dict "note": "uuid": note_id "patient": "id": pt_id IMMUNIZE__CODING__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMMUNIZE__CODING__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMMUNIZE__GIVEN_BY__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMMUNIZE__GIVEN_BY__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMMUNIZE__LOT_NUMBER__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] IMMUNIZE__LOT_NUMBER__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Instruct Command INSTRUCT_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "instruct": dict "narrative": str "note": "uuid": note_id "patient": "id": pt_id INSTRUCT_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "instruct": dict "narrative": str "note": "uuid": note_id "patient": "id": pt_id INSTRUCT_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "instruct": dict "narrative": str "note": "uuid": note_id "patient": "id": pt_id INSTRUCT_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id INSTRUCT_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "instruct": dict "narrative": str "note": "uuid": note_id "patient": "id": pt_id INSTRUCT_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "instruct": dict "narrative": str "note": "uuid": note_id "patient": "id": pt_id INSTRUCT_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "instruct": dict "narrative": str "note": "uuid": note_id "patient": "id": pt_id INSTRUCT_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "instruct": dict "narrative": str "note": "uuid": note_id "patient": "id": pt_id INSTRUCT_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "instruct": dict "narrative": str "note": "uuid": note_id "patient": "id": pt_id INSTRUCT_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "instruct": dict "narrative": str "note": "uuid": note_id "patient": "id": pt_id INSTRUCT_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "instruct": dict "narrative": str "note": "uuid": note_id "patient": "id": pt_id INSTRUCT_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "instruct": dict "narrative": str "note": "uuid": note_id "patient": "id": pt_id INSTRUCT_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "instruct": dict "narrative": str "note": "uuid": note_id "patient": "id": pt_id INSTRUCT_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "instruct": dict "narrative": str "note": "uuid": note_id "patient": "id": pt_id INSTRUCT_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "instruct": dict "narrative": str "note": "uuid": note_id "patient": "id": pt_id INSTRUCT__INSTRUCT__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] INSTRUCT__INSTRUCT__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Lab Order Command LAB_ORDER_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "lab_partner": dict "tests": list[dict] "ordering_provider": dict "diagnosis": list[dict] "fasting_status": bool "comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_ORDER_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "lab_partner": dict "tests": list[dict] "ordering_provider": dict "diagnosis": list[dict] "fasting_status": bool "comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_ORDER_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "lab_partner": dict "tests": list[dict] "ordering_provider": dict "diagnosis": list[dict] "fasting_status": bool "comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_ORDER_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id LAB_ORDER_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "lab_partner": dict "tests": list[dict] "ordering_provider": dict "diagnosis": list[dict] "fasting_status": bool "comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_ORDER_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "lab_partner": dict "tests": list[dict] "ordering_provider": dict "diagnosis": list[dict] "fasting_status": bool "comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_ORDER_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "lab_partner": dict "tests": list[dict] "ordering_provider": dict "diagnosis": list[dict] "fasting_status": bool "comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_ORDER_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "lab_partner": dict "tests": list[dict] "ordering_provider": dict "diagnosis": list[dict] "fasting_status": bool "comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_ORDER_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "lab_partner": dict "tests": list[dict] "ordering_provider": dict "diagnosis": list[dict] "fasting_status": bool "comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_ORDER_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "lab_partner": dict "tests": list[dict] "ordering_provider": dict "diagnosis": list[dict] "fasting_status": bool "comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_ORDER_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "lab_partner": dict "tests": list[dict] "ordering_provider": dict "diagnosis": list[dict] "fasting_status": bool "comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_ORDER_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "lab_partner": dict "tests": list[dict] "ordering_provider": dict "diagnosis": list[dict] "fasting_status": bool "comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_ORDER_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "lab_partner": dict "tests": list[dict] "ordering_provider": dict "diagnosis": list[dict] "fasting_status": bool "comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_ORDER_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "lab_partner": dict "tests": list[dict] "ordering_provider": dict "diagnosis": list[dict] "fasting_status": bool "comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_ORDER_COMMAND__PRE_SEND --- Fires from Canvas right before a lab order's FHIR `RequestGroup` is built and POSTed to Health Gorilla. Plugins may respond with one or more [HealthGorillaLabOrderOverride](/sdk/effect-health-gorilla-lab-order-override/) effects to inject account numbers, bill-to, performer organization, sub-tenant, or location into the outbound payload. Target object| Context object "id": laborder_id "type": [LabOrder](/sdk/data-labs/#laborder) | "lab_order": "id": laborder_id "uuid": laborder_id "lab_partner": str "note": "id": note_id "uuid": note_id "patient": "id": pt_id HEALTH_GORILLA_LAB_ORDER_PREPARED --- Fires from Canvas right after the outbound Health Gorilla FHIR `RequestGroup` dict is constructed and right before it is POSTed to Health Gorilla. Read-only — any effects returned by handlers are discarded. Complements `LAB_ORDER_COMMAND__PRE_SEND`, which fires before the build and accepts override effects. Target object| Context object "id": laborder_id "type": [LabOrder](/sdk/data-labs/#laborder) | "lab_order": "id": laborder_id "uuid": laborder_id "lab_partner": str "note": "id": note_id "uuid": note_id "patient": "id": pt_id "request_group": dict LAB_ORDER_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "lab_partner": dict "tests": list[dict] "ordering_provider": dict "diagnosis": list[dict] "fasting_status": bool "comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_ORDER__DIAGNOSIS__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] LAB_ORDER__DIAGNOSIS__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] LAB_ORDER__LAB_PARTNER__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] LAB_ORDER__LAB_PARTNER__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] LAB_ORDER__ORDERING_PROVIDER__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] LAB_ORDER__ORDERING_PROVIDER__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] LAB_ORDER__TESTS__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] LAB_ORDER__TESTS__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Lab Review Command LAB_REVIEW_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_REVIEW_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_REVIEW_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_REVIEW_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_REVIEW_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_REVIEW_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_REVIEW_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_REVIEW_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_REVIEW_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_REVIEW_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_REVIEW_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_REVIEW_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "report": str "message_to_patient": str "communication_method": str "internal_comment": str "note": "uuid": note_id "patient": "id": pt_id LAB_REVIEW__REPORT__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] LAB_REVIEW__REPORT__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] LAB_REVIEW__COMMUNICATION_METHOD__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] LAB_REVIEW__COMMUNICATION_METHOD__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Medical History Command MEDICAL_HISTORY_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "past_medical_history": dict "approximate_start_date": "date": str "input": str "approximate_end_date": "date": str "input": str "show_on_condition_list": bool "comments": str "note": "uuid": note_id "patient": "id": pt_id MEDICAL_HISTORY_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "past_medical_history": dict "approximate_start_date": "date": str "input": str "approximate_end_date": "date": str "input": str "show_on_condition_list": bool "comments": str "note": "uuid": note_id "patient": "id": pt_id MEDICAL_HISTORY_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "past_medical_history": dict "approximate_start_date": "date": str "input": str "approximate_end_date": "date": str "input": str "show_on_condition_list": bool "comments": str "note": "uuid": note_id "patient": "id": pt_id MEDICAL_HISTORY_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id MEDICAL_HISTORY_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "past_medical_history": dict "approximate_start_date": "date": str "input": str "approximate_end_date": "date": str "input": str "show_on_condition_list": bool "comments": str "note": "uuid": note_id "patient": "id": pt_id MEDICAL_HISTORY_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "past_medical_history": dict "approximate_start_date": "date": str "input": str "approximate_end_date": "date": str "input": str "show_on_condition_list": bool "comments": str "note": "uuid": note_id "patient": "id": pt_id MEDICAL_HISTORY_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "past_medical_history": dict "approximate_start_date": "date": str "input": str "approximate_end_date": "date": str "input": str "show_on_condition_list": bool "comments": str "note": "uuid": note_id "patient": "id": pt_id MEDICAL_HISTORY_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "past_medical_history": dict "approximate_start_date": "date": str "input": str "approximate_end_date": "date": str "input": str "show_on_condition_list": bool "comments": str "note": "uuid": note_id "patient": "id": pt_id MEDICAL_HISTORY_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "past_medical_history": dict "approximate_start_date": "date": str "input": str "approximate_end_date": "date": str "input": str "show_on_condition_list": bool "comments": str "note": "uuid": note_id "patient": "id": pt_id MEDICAL_HISTORY_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "past_medical_history": dict "approximate_start_date": "date": str "input": str "approximate_end_date": "date": str "input": str "show_on_condition_list": bool "comments": str "note": "uuid": note_id "patient": "id": pt_id MEDICAL_HISTORY_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "past_medical_history": dict "approximate_start_date": "date": str "input": str "approximate_end_date": "date": str "input": str "show_on_condition_list": bool "comments": str "note": "uuid": note_id "patient": "id": pt_id MEDICAL_HISTORY_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "past_medical_history": dict "approximate_start_date": "date": str "input": str "approximate_end_date": "date": str "input": str "show_on_condition_list": bool "comments": str "note": "uuid": note_id "patient": "id": pt_id MEDICAL_HISTORY_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "past_medical_history": dict "approximate_start_date": "date": str "input": str "approximate_end_date": "date": str "input": str "show_on_condition_list": bool "comments": str "note": "uuid": note_id "patient": "id": pt_id MEDICAL_HISTORY_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "past_medical_history": dict "approximate_start_date": "date": str "input": str "approximate_end_date": "date": str "input": str "show_on_condition_list": bool "comments": str "note": "uuid": note_id "patient": "id": pt_id MEDICAL_HISTORY_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "past_medical_history": dict "approximate_start_date": "date": str "input": str "approximate_end_date": "date": str "input": str "show_on_condition_list": bool "comments": str "note": "uuid": note_id "patient": "id": pt_id MEDICAL_HISTORY__APPROXIMATE_END_DATE__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] MEDICAL_HISTORY__APPROXIMATE_END_DATE__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] MEDICAL_HISTORY__APPROXIMATE_START_DATE__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] MEDICAL_HISTORY__APPROXIMATE_START_DATE__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] MEDICAL_HISTORY__PAST_MEDICAL_HISTORY__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[ConditionSearchResult] MEDICAL_HISTORY__PAST_MEDICAL_HISTORY__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Medication Statement Command MEDICATION_STATEMENT_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id MEDICATION_STATEMENT_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id MEDICATION_STATEMENT_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id MEDICATION_STATEMENT_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id MEDICATION_STATEMENT_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id MEDICATION_STATEMENT_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id MEDICATION_STATEMENT_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id MEDICATION_STATEMENT_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id MEDICATION_STATEMENT_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id MEDICATION_STATEMENT_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id MEDICATION_STATEMENT_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id MEDICATION_STATEMENT_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id MEDICATION_STATEMENT_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id MEDICATION_STATEMENT_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id MEDICATION_STATEMENT_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "medication": dict "sig": str "note": "uuid": note_id "patient": "id": pt_id MEDICATION_STATEMENT__MEDICATION__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[MedicationSearchResult] MEDICATION_STATEMENT__MEDICATION__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[MedicationSearchResult] #### Perform Command PERFORM_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "perform": dict "notes": str "note": "uuid": note_id "patient": "id": pt_id PERFORM_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "perform": dict "notes": str "note": "uuid": note_id "patient": "id": pt_id PERFORM_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "perform": dict "notes": str "note": "uuid": note_id "patient": "id": pt_id PERFORM_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id PERFORM_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "perform": dict "notes": str "note": "uuid": note_id "patient": "id": pt_id PERFORM_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "perform": dict "notes": str "note": "uuid": note_id "patient": "id": pt_id PERFORM_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "perform": dict "notes": str "note": "uuid": note_id "patient": "id": pt_id PERFORM_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "perform": dict "notes": str "note": "uuid": note_id "patient": "id": pt_id PERFORM_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "perform": dict "notes": str "note": "uuid": note_id "patient": "id": pt_id PERFORM_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "perform": dict "notes": str "note": "uuid": note_id "patient": "id": pt_id PERFORM_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "perform": dict "notes": str "note": "uuid": note_id "patient": "id": pt_id PERFORM_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "perform": dict "notes": str "note": "uuid": note_id "patient": "id": pt_id PERFORM_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "perform": dict "notes": str "note": "uuid": note_id "patient": "id": pt_id PERFORM_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "perform": dict "notes": str "note": "uuid": note_id "patient": "id": pt_id PERFORM_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "perform": dict "notes": str "note": "uuid": note_id "patient": "id": pt_id PERFORM__PERFORM__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] PERFORM__PERFORM__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Physical Exam Command PHYSICAL_EXAM_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "questionnaire": dict "note": "uuid": note_id "patient": "id": pt_id PHYSICAL_EXAM_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "questionnaire": dict "note": "uuid": note_id "patient": "id": pt_id PHYSICAL_EXAM_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "questionnaire": dict "note": "uuid": note_id "patient": "id": pt_id PHYSICAL_EXAM_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id PHYSICAL_EXAM_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "questionnaire": dict "note": "uuid": note_id "patient": "id": pt_id PHYSICAL_EXAM_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "questionnaire": dict "note": "uuid": note_id "patient": "id": pt_id PHYSICAL_EXAM_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "questionnaire": dict "note": "uuid": note_id "patient": "id": pt_id PHYSICAL_EXAM_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "questionnaire": dict "note": "uuid": note_id "patient": "id": pt_id PHYSICAL_EXAM_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "questionnaire": dict "note": "uuid": note_id "patient": "id": pt_id PHYSICAL_EXAM_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "questionnaire": dict "note": "uuid": note_id "patient": "id": pt_id PHYSICAL_EXAM_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "questionnaire": dict "note": "uuid": note_id "patient": "id": pt_id PHYSICAL_EXAM_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "questionnaire": dict "note": "uuid": note_id "patient": "id": pt_id PHYSICAL_EXAM_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "questionnaire": dict "note": "uuid": note_id "patient": "id": pt_id PHYSICAL_EXAM_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "questionnaire": dict "note": "uuid": note_id "patient": "id": pt_id PHYSICAL_EXAM_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "questionnaire": dict "note": "uuid": note_id "patient": "id": pt_id PHYSICAL_EXAM__QUESTIONNAIRE__POST_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] PHYSICAL_EXAM__QUESTIONNAIRE__PRE_SEARCH --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "search_term": str "user": { "staff": staff_key } "results": list[dict] #### Plan Command PLAN_COMMAND__POST_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id PLAN_COMMAND__POST_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id PLAN_COMMAND__POST_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id PLAN_COMMAND__AVAILABLE_ACTIONS --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "actions": "name": string "user": "staff": staff_id PLAN_COMMAND__POST_VALIDATION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id PLAN_COMMAND__POST_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id PLAN_COMMAND__POST_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id PLAN_COMMAND__POST_INSERTED_INTO_NOTE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id PLAN_COMMAND__POST_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id PLAN_COMMAND__PRE_COMMIT --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id PLAN_COMMAND__PRE_DELETE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id PLAN_COMMAND__PRE_ENTER_IN_ERROR --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id PLAN_COMMAND__PRE_EXECUTE_ACTION --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id PLAN_COMMAND__PRE_ORIGINATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id PLAN_COMMAND__PRE_UPDATE --- Target object| Context object "id": command_uuid "type": [Command](/sdk/data-command/) | "fields": "narrative": str "note": "uuid": note_id "patient": "id": pt_id #### POC Lab Test Command POC Lab Test commands carry a `test_values|