Applications

Applications put your own tools inside Canvas, so staff and patients can use them without leaving the workflow they’re in. An application can be a dashboard, a form, a third-party tool loaded in an iframe, or a full page you build in your plugin.

Applications can show up wherever they fit the work:

  • In the app drawer, on a patient’s chart or across Canvas
  • As a tab in the patient chart, next to Chart and Profile
  • In the provider menu
  • In the patient portal menu, for patients
  • In the Provider Companion

An application opens your content when a user selects it, and it can show a notification badge to flag what’s waiting. Its scope decides where it appears.

Implementing an Application #

To add an application, create a handler class that inherits from Application, then register it in your manifest.

Methods #

  • on_open() (required) runs when a user opens the application. It usually returns a LaunchModalEffect with a url to load in an iframe or content to render directly. Set a title so users can recognize the application when it’s minimized. Return one Effect or a list of them.
  • on_context_change() (optional) runs when the user moves to another page while the application is open. Return an Effect, a list of them, or None to do nothing. See Context Change Events for when it fires and what it receives.

Opening for the current patient #

When an application opens on a patient chart, on_open() can read the current patient from self.context, which looks like {'patient': {'id': '24cfe22ecf74420fb82dc40e44ca6166'}}. Use it to open your application for that patient:

from canvas_sdk.effects import Effect
from canvas_sdk.effects.launch_modal import LaunchModalEffect
from canvas_sdk.handlers.application import Application


class MyApplication(Application):
    """An embeddable application that can be registered to Canvas."""

    def on_open(self) -> Effect:
        """Handle the on_open event."""
        patient_id = self.context['patient']['id']
        patient_specific_url = f"https://example.com/patient-specific-application?patient={patient_id}"

        return LaunchModalEffect(
            url=patient_specific_url,
            target=LaunchModalEffect.TargetType.DEFAULT_MODAL,
        ).apply()

Registering the application #

Your CANVAS_MANIFEST.json must also describe the application. Reference your class in the “applications” section of the components so your application is registered in the app drawer on plugin installation.

This is also where you can define the title and icon that displays your app in the app drawer. The icon will be rendered at 48px by 48px, so should be square and simple enough to not lose detail at that size.

Example #

This application opens in the right chart pane and reloads with claim or queue details as the user moves through revenue pages:

from canvas_sdk.effects import Effect
from canvas_sdk.effects.launch_modal import LaunchModalEffect
from canvas_sdk.handlers.application import Application


class IFrameApp(Application):
    def on_open(self) -> Effect | list[Effect]:
        return LaunchModalEffect(url=f"https://www.your-iframe-app.com",
            target=LaunchModalEffect.TargetType.RIGHT_CHART_PANE, title="Your Iframe App").apply()

    def on_context_change(self) -> Effect | list[Effect] | None:
        # Access the current URL that triggered the context change
        current_url = self.event.context.get("url", "")

        # Handle claim-specific context
        if claim := self.event.context.get("claim"):
            claim_id = claim["id"]
            return LaunchModalEffect(
                url=f"https://www.your-iframe-app.com?claim_id={claim_id}&source_url={current_url}",
                target=LaunchModalEffect.TargetType.RIGHT_CHART_PANE,
                title=f"Your Iframe App - Claim {claim_id}"
            ).apply()

        # Handle queue-specific context
        if queue := self.event.context.get("claim_queue"):
            queue_id = queue["dbid"]
            return LaunchModalEffect(
                url=f"https://www.your-iframe-app.com?queue_id={queue_id}&source_url={current_url}",
                target=LaunchModalEffect.TargetType.RIGHT_CHART_PANE,
                title=f"Your Iframe App - Queue {queue_id}"
            ).apply()

        # Handle general revenue page context
        if current_url.startswith("/revenue"):
            return LaunchModalEffect(
                url=f"https://www.your-iframe-app.com?page=revenue&source_url={current_url}",
                target=LaunchModalEffect.TargetType.RIGHT_CHART_PANE,
                title="Your Iframe App - Revenue"
            ).apply()

        # Return None when no relevant context - this will result in an empty effect list
        return None

Context Change Events #

Context change events are automatically triggered when users navigate between different URLs within Canvas. This feature allows your applications to react dynamically to the user’s current context, providing relevant information and functionality based on where they are in the system.

Event Triggers #

Context change events are currently supported for revenue workflows and are triggered when:

  • A user navigates to a different URL within Canvas
  • The application is already open and running
  • The new URL is within the /revenue namespace

Context Data Structure #

When a context change event occurs, your on_context_change() method receives contextual information through self.event.context:

{
    "url": "/revenue/claims/123",           # Current URL that triggered the event
    "patient": {"id": "patient_key"},       # Patient information (when applicable)
    "user": {...},                          # User information
    "claim": {"id": "external_claim_id"},   # Claim context (for /revenue/claims/<id>)
    "claim_queue": {"dbid": "queue_id"}     # Queue context (for /revenue/queues/<id>)
}

Supported URL Patterns #

URL PatternContext ProvidedDescription
/revenueBase context onlyGeneral revenue page
/revenue/claims/<id>claim object with externally exposable IDSpecific claim details page
/revenue/queues/<id>claim_queue object with database IDSpecific queue management page

Best Practices #

  1. Always check for context existence: Use safe dictionary access patterns to avoid KeyErrors
  2. Handle multiple context types: Your application may receive different types of context based on the URL
  3. Return None appropriately: When no relevant action is needed, return None to avoid unnecessary effects

Advanced Example #

from canvas_sdk.effects import Effect
from canvas_sdk.effects.launch_modal import LaunchModalEffect
from canvas_sdk.handlers.application import Application


class AdvancedRevenueApp(Application):
    def on_open(self) -> Effect | list[Effect]:
        return LaunchModalEffect(
            url="https://www.your-app.com/dashboard",
            target=LaunchModalEffect.TargetType.RIGHT_CHART_PANE,
            title="Revenue Analytics"
        ).apply()

    def on_context_change(self) -> Effect | list[Effect] | None:
        current_url = self.event.context.get("url", "")
        patient = self.event.context.get("patient", {})
        user = self.event.context.get("user", {})

        # Build base parameters
        params = {
            "source_url": current_url,
            "user_id": user.get("id", ""),
            "patient_id": patient.get("id", "")
        }

        # Handle specific contexts
        if claim := self.event.context.get("claim"):
            params["claim_id"] = claim["id"]
            params["view"] = "claim_details"
            title = f"Revenue Analytics - Claim {claim['id']}"

        elif queue := self.event.context.get("claim_queue"):
            params["queue_id"] = queue["dbid"]
            params["view"] = "queue_management"
            title = f"Revenue Analytics - Queue {queue['dbid']}"

        elif current_url.startswith("/revenue"):
            params["view"] = "revenue_overview"
            title = "Revenue Analytics - Overview"

        else:
            # No relevant context for this application
            return None

        # Build query string
        query_string = "&".join(f"{k}={v}" for k, v in params.items() if v)

        return LaunchModalEffect(
            url=f"https://www.your-app.com/revenue?{query_string}",
            target=LaunchModalEffect.TargetType.RIGHT_CHART_PANE,
            title=title
        ).apply()

Application Scopes #

The scope attribute determines where your application is visible within Canvas. The following scopes are available:

ScopeDescription
patient_specificVisible only within a patient’s chart in the app drawer
globalVisible outside of patient charts in the app drawer
full_chartDisplayed as a tab in the patient chart navigation menu alongside Chart and Profile
provider_menu_itemDisplayed as a menu item in the provider menu
portal_menu_itemDisplayed as a menu item in the patient portal
provider_companionVisible on the Provider Companion main page (legacy, use provider_companion_global for new apps)
provider_companion_globalIn the app launcher on the Provider Companion main page
provider_companion_patient_specificAs a tab on a patient’s page in the Provider Companion
provider_companion_note_specificAs a tab within an opened note in the Provider Companion

Who sees an application #

Applications in the global, patient_specific, and provider_menu_item scopes are shown to every staff member, and portal_menu_item applications are shown to every patient in the patient portal. To limit what a user can do, check who opened the application in on_open(), using the user in self.event.context, and show a message instead of the content when that user shouldn’t have access.

When a patient opens a portal_menu_item application, the event context identifies them only through user, for example {"type": "Patient", "id": "..."}. Canvas sets url to None and leaves out patient and other page context, so use the user ID to look up the patient’s records.

Linking to a portal application #

A portal_menu_item application’s URL ends in its identifier, base64-encoded. For example, .../app/application/cGF0aWVudF9wb3J0YWxfY29uc2VudF9mb3Jtcy4uLg== decodes to plugin_name.module.path:ClassName. Because the identifier is made of the plugin name, module path, and class name, renaming any of them changes the URL and breaks links to it. The browser’s address bar also stays the same as the patient moves between applications.

To send a patient to a portal application from a plugin, return a RedirectEffect with the application’s application_id instead of building its URL. A redirect to an /app/application/... URL is blocked.

Full Chart Scope #

Applications with the full_chart scope appear as navigation tabs at the top of the patient chart, alongside the default “Chart” and “Profile” tabs. This is ideal for building comprehensive patient-level views or dashboards.

{
  "class": "my_plugin.apps.analytics:PatientAnalytics",
  "name": "Analytics",
  "description": "Patient analytics dashboard",
  "icon": "/assets/analytics-icon.png",
  "scope": "full_chart"
}

A full_chart application is also the way to give custom patient data its own layout. The patient metadata form shows fields as one flat list in the profile, with no sections or headings. To group fields your own way, build the form in a full_chart application and save the values with the patient metadata effect from a SimpleAPI endpoint, so they’re stored the same way.

Provider Companion Applications #

Provider companion applications run in the Canvas provider companion — a mobile-optimized, provider-facing surface. They use the Application handler with one of three companion scopes (provider_companion_global, provider_companion_patient_specific, provider_companion_note_specific) declared in the manifest. The legacy provider_companion scope continues to work and is treated the same as provider_companion_global.

See Provider Companion for the full guide — scope-by-scope examples, event context, code sharing across scopes, originating commands from a note, modal dismissal, and mobile UX guidance.

Embedded Applications #

Embedded applications are handler-based applications that render inside a specific Canvas surface rather than appearing in the app drawer. There are five kinds:

  • Note Applications, which add tabs inside a note
  • Scheduling Applications, which replace the built-in scheduling modal
  • Docked Applications, which pin a persistent pane to a window edge
  • Provider Menu Applications, which add an entry to the provider side menu
  • Panel Applications, which add an icon to the panel bar

They are declared under handlers (not applications), take no scope, and create no application record. A ProviderMenuApplication is separate from a drawer application that uses the provider_menu_item scope, and a PanelApplication is separate from a drawer application shown with show_in_panel; each pair can coexist.

See Embedded Applications for the full guide.

Panel Display #

If you want to increase your application’s visibility and display it alongside other panel buttons (instead of in the applications drawer), you can add the show_in_panel attribute. If you’ve added more than one application to that panel, you can set their priorities using the panel_priority attribute.

For security reasons you also need to specify the domains that will be loaded within the iframe, or they will not be rendered. For more info on the format of the url_permissions field, check the Additional Configuration for LaunchModalEffect.

Here’s what your CANVAS_MANIFEST.json might look like:

{
  "sdk_version": "0.1.4",
  "plugin_version": "0.0.1",
  "name": "my_application",
  "description": "This is a very nice application",
  "url_permissions": [
    {
      "url": "https://example.com/",
      "permissions": ["ALLOW_SAME_ORIGIN", "MICROPHONE", "SCRIPTS", "CAMERA", "CLIPBOARD_READ", "CLIPBOARD_WRITE"]
    }
  ],
  "components": {
    "handlers": [],
    "applications": [
      {
        "class": "my_application.apps.iframe:IFrameApp",
        "name": "My Application",
        "description": "Test App for patients",
        "icon": "/assets/cappuccino.png",
        "scope": "patient_specific",
        "show_in_panel": true,
        "panel_priority": 100
      }
    ],
    "commands": [],
    "content": [],
    "effects": [],
    "views": []
  },
  "variables": [],
  "tags": {},
  "references": [],
  "license": "",
  "diagram": false,
  "readme": "./README.md"
}

Installing and updating applications #

When you install or upgrade a plugin, Canvas reconciles its applications to match CANVAS_MANIFEST.json. Canvas creates each application declared under components.applications that is new and updates each one that already exists. Canvas removes any registered application that the manifest no longer declares. The removed application’s entry disappears from the app drawer and from any menu it appeared in, such as the provider menu, and its stored icon is deleted.

Reconciliation affects only the plugin being installed. Applications that belong to other plugins are left unchanged.

An application’s identity is its class value, the module:ClassName string, and Canvas matches applications by class during reconciliation:

  • Editing only display fields (name, description, icon, scope, and so on) while keeping the same class updates the existing application in place.
  • Changing the class value, by editing either the module path or the class name, removes the old application and adds a new one rather than updating the existing application in place.

Note: Per-application instance settings such as Open on load attach to a specific application. Because changing an application’s class creates a new application, those settings do not carry over to the new application.

Application changes apply together with the plugin’s other install or upgrade changes, such as commands and questionnaires, as a single all-or-nothing operation, so if an install fails, the plugin’s applications are left unchanged.

Opening an Application on Load #

You can configure an application to open automatically, without the user clicking its icon, by enabling the Open on load setting for that application in your instance settings.

To enable it, go to the Plugins_IO > Applications section of your instance settings (/admin/plugin_io/application/), open the application you want, check Open on load, and save. If you don’t have access to this setting, reach out to Canvas Support.

Behavior depends on the application’s scope:

ScopeWhen it opens
globalAutomatically when the app shell first loads.
patient_specificAutomatically when a patient chart is opened.

This is an instance-level setting configured per application in your instance settings. It is not part of CANVAS_MANIFEST.json, so the value you set is preserved when the plugin is reinstalled or updated.

Note: This is distinct from a Note Application’s open_by_default(), which controls which tab is active when a note is viewed. Open on load controls whether a global or patient_specific application opens automatically on app/chart load.

Notification Badges #

You can display a notification badge — a small count — on an application in these scopes:

  • global and patient_specific: on the icon in the app drawer, or on the panel when a patient_specific application sets show_in_panel.
  • provider_menu_item: next to the label in the provider menu.
  • portal_menu_item: next to the label in the patient portal menu.

A badge is useful for surfacing how many items are waiting for attention, such as unread messages or open tasks. Applications in other scopes (full_chart and the Provider Companion scopes) do not display badges.

Initial count on load #

Override compute_notification_badge() on your Application handler to provide the count shown when Canvas loads applications. Return an integer to show a badge, or None (the default) to show no badge. A count of 0 shows no badge.

from canvas_sdk.effects import Effect
from canvas_sdk.effects.launch_modal import LaunchModalEffect
from canvas_sdk.handlers.application import Application
from canvas_sdk.v1.data.task import Task, TaskStatus


class InboxApp(Application):
    def on_open(self) -> Effect | list[Effect]:
        return LaunchModalEffect(
            url="https://www.your-app.com/inbox",
            title="Inbox",
        ).apply()

    def compute_notification_badge(self) -> int | None:
        """Return the badge count shown on the icon when applications load."""
        staff_id = self.event.context.get("staff", {}).get("id")
        if not staff_id:
            return None
        return Task.objects.filter(assignee__id=staff_id, status=TaskStatus.OPEN).count()

When the application is rendered on a patient chart (patient_specific scope), the event context also carries the patient, so you can compute a count specific to the staff member and the patient they are viewing:

from canvas_sdk.v1.data.task import Task, TaskStatus

def compute_notification_badge(self) -> int | None:
    staff_id = self.event.context.get("staff", {}).get("id")
    patient_id = self.event.context.get("patient", {}).get("id")
    if not (staff_id and patient_id):
        return None
    return Task.objects.filter(
        assignee__id=staff_id, patient__id=patient_id, status=TaskStatus.OPEN
    ).count()

For a portal_menu_item application, the count is computed for the patient logged in to the portal. The event context carries that patient and no staff member:

from canvas_sdk.v1.data.task import Task, TaskStatus

def compute_notification_badge(self) -> int | None:
    patient_id = self.event.context.get("patient", {}).get("id")
    if not patient_id:
        return None
    return Task.objects.filter(patient__id=patient_id, status=TaskStatus.OPEN).count()

The badge event context contains:

KeyDescription
staffA dict with the staff id and type (present for staff-facing apps).
patientA dict with the patient id and type (present on a patient chart, and in the patient portal for the logged-in patient).

Note: Note Applications (NoteApplication) do not support notification badges.

Live updates #

To change the count after load — for example, in response to a new task or message — emit an ApplicationNotificationBadge effect from any event handler. The badge updates in real time without the user reloading the page. See the Application Notification Badge effect for details.

Limitations and patterns #

Third-party scripts only run inside a plugin iframe #

Canvas does not provide a way to inject application-wide JavaScript (for example, Segment or Sentry snippets) into the Canvas front end. Third-party scripts can only run inside the iframe of a plugin’s own application.

Application iframes and cookies #

An application iframe can use cookies for its own domain only when its URL’s entry in url_permissions includes ALLOW_SAME_ORIGIN. Without it, each launch behaves like an incognito session, so a user may have to sign in to your application every time it opens.

Replacing a built-in view #

When a built-in Canvas view doesn’t fit a workflow, an application can replace it with your own, built from the same data and actions the SDK exposes. Read what you need with the data module, make changes with effects returned from a SimpleAPI endpoint in the same plugin, and lay out the page however the workflow needs.

For example, to present tasks with locked fields, predefined dropdowns, or custom labels beyond what the built-in Task command offers, build an application that opens on the right side of a note. Read tasks with the Task data model, and create or update them with the task effects. Give these tasks a label of their own so your application can find them and staff can filter them out of the general Tasks list.