# Medplum > Fast and easy healthcare dev This file contains all documentation content in a single document following the llmstxt.org standard. ## Access Policies ### Introduction This document describes the core concepts of Medplum Access Controls. Security and access controls are notoriously difficult. Complex business and regulatory requirements often lead to a mess of incomprehensible rules. Medplum strives to create a simple and understandable model, which is flexible enough to handle all unique security needs. ### Core Model All resources exist within a "Project". A project is a top-level container. In general, each healthcare organization will have one project for all of their resources. Every user account can have one or more "Project Memberships". A project membership represents access to resources within a project. The user can either be granted access to all resources within the project, or limited access to a set of compartments. ### Access Policies Users within a Project can be assigned Access Policies. Access Policies are an advanced method of restricting access to certain resource types or even certain fields within a resource type. Access policies allow you to: - Block access to any resource type - Grant read only access to any resource type - Grant read/write access to any resource type - Grant read only access to any property - Grant read/write access to any property Access policies also allow you to grant access by "Compartment". ### Examples #### Resource Type The following access policy grants read/write access to _only_ the "Patient" resource type: ```json { "resourceType": "AccessPolicy", "name": "Patient Example", "resource": [ { "resourceType": "Patient" } ] } ``` #### Criteria-based Access Control You can narrow the set of resources the user has access to by using the `criteria` field. The following policy uses a [FHIR Search Query](/docs/search/basic-search) to grant access only to [`Patient`](/docs/api/fhir/resources/patient) resources who live in California. See the [Search documentation](/docs/search/basic-search) for more information on the types of filtering available. ```json { "resourceType": "AccessPolicy", "name": "Criteria Based Access Policy", "resource": [ { "resourceType": "Patient", "criteria": "Patient?address-state=CA" } ] } ``` :::warning[Limitations] While Medplum `AccessPolicies` use the [FHIR search syntax](/docs/search), it does not implement the full search specification. Specifically, the `criteria` syntax has the following limitations: - Only `:not` and `:missing` [modifiers](/docs/search/basic-search#search-modifiers) are allowed. - [Chained searches](/docs/search/chained-search#forward-chained-search) are **not** allowed. ::: #### Read-only Access The following access policy grants read-only access to the [`Patient`](/docs/api/fhir/resources/patient) resource type: ```json { "resourceType": "AccessPolicy", "name": "Patient Example", "resource": [ { "resourceType": "Patient", "readonly": true } ] } ``` Similarly, the following access policy grants read/write access to the Patient resource, and also grants _only_ read access to all other resources: ```json { "resourceType": "AccessPolicy", "name": "Read Only Example", "resource": [ { "resourceType": "Patient" }, { "resourceType": "*", "readonly": true } ] } ``` Attempting to modify a read-only resource will result in an HTTP [`403 Forbidden`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403) response. #### FHIR Interactions The above read-only access is special case of the more general ability to control which interactions are allowed for a set of resources. The `AccessPolicy.resource.interaction` field can specify a subset of [FHIR interactions](http://hl7.org/fhir/R4/codesystem-restful-interaction.html) that can be performed on resources of the given type. The following are equivalent: ```js { "resourceType": "AccessPolicy", "name": "Read-only modes", "resource": [ // Legacy shorthand { "resourceType": "*", "readonly": true, }, // Explicit list of interactions { "resourceType": "*", "interaction": ["read", "search", "history", "vread"] } ] } ``` The `interaction` array can contain any of the supported interaction types: - `create`: Write new resources - `read`: Read a specific resource by ID - `update`: Update an existing resource, including patch operations - `delete`: Soft-delete resources - `search`: Search for resources of the given type - `history`: View the list of previous versions of a resource - `vread`: View a specific previous version of a resource :::tip[Related interactions] Some FHIR interactions are used in concert by the server, and so should be considered together: - [Upserts](/docs/fhir-datastore/working-with-fhir#upsert) require `search` to identify existing resources, in addition to both `create` and `update` - `search` access may be required as part of a `create` or `update` if the resource contains any [Conditional references](/docs/migration/convert-to-fhir#linking-data-using-conditional-references), in order to resolve references to individual resources - `read` access to referenced resource types is required in `create` and `update` interactions, if [`Project.checkReferencesOnWrite`](/docs/api/fhir/medplum/project) is set - `history` and `vread` are often specified together, to grant access to the entire [version history](/docs/fhir-datastore/resource-history) of resources ::: For example, this policy allows creating new resources and viewing any resources for which the ID is known, but will prevent searching for or modifying existing resources: ```json { "resourceType": "AccessPolicy", "name": "Write-only Sandbox", "resource": [ { "resourceType": "*", "interaction": ["create", "read"] } ] } ``` #### Read-only Elements The following access policy grants read-only access to the `Patient.name` and `Patient.address` elements: ```json { "resourceType": "AccessPolicy", "name": "Patient Example", "resource": [ { "resourceType": "Patient", "readonlyFields": ["name", "address"] } ] } ``` #### Hidden Elements The following access policy grants read-only access to the "Patient" resource type, but hides "name" and "address": ```json { "resourceType": "AccessPolicy", "name": "Patient Example", "resource": [ { "resourceType": "Patient", "hiddenFields": ["name", "address"] } ] } ``` #### Write Constraints Constraints on writes to a resource can also be specified using [FHIRPath expressions][fhirpath] in the `AccessPolicy.resource.writeConstraint` field. These expressions may contain the special variable `%before` to refer to the resource as it existed before the write. Any property accesses will by default refer to the resource as it exists with updates applied, but the `%after` variable is also provided for convenience. :::tip[] In case of a resource being created, `%before` will be undefined, so any expressions that refer to `%before` must account for this case. To select only updates or only creates, prefix the criteria with `%before.exists() implies` or `%before.exists().not() implies` respectively. ::: For example, an access policy with write constraints could be used to manage state transitions by prohibiting changing the status once a resource is marked as `final`, and ensure that a `subject` is set when the resource is finalized: ```json { "resourceType": "AccessPolicy", "name": "Write Constraints Access Policy", "resource": [ { "resourceType": "Observation", "writeConstraint": [ { "language": "text/fhirpath", "expression": "%before.exists() implies %before.status != 'final'" }, { "language": "text/fhirpath", "expression": "status = 'final' implies subject.exists()" } ] } ] } ``` [fhirpath]: https://hl7.org/fhirpath/#expressions #### Compartments All resources can be tagged with one or more "Compartments". A compartment is simply a group of resources. Importantly, compartments are not mutually exclusive. A resource can (and often will) exist in multiple compartments. For example, consider an "Observation" resource representing a blood pressure measurement. That Observation resource will fall into the following Compartments: - "Patient" - for the "subject" of the observation - "Practitioner" - for the "performer" of the observation - "Encounter" - for the "encounter" of the observation Resources are automatically assigned to compartments based on rules. Currently, the Medplum server automatically assigns resources to the "Patient" compartment. You can find the full definition of the patient compartment [here](https://hl7.org/fhir/R4/compartmentdefinition-patient.html). You can use compartments to succinctly create an [AccessPolicy](/docs/api/fhir/medplum/accesspolicy) for resources related to a single patient. The AccessPolicy below grants access to all `Observation` resources that belong to `Patient/xyz`. The example takes advantage of the [`_compartment` search parameter](/docs/search/advanced-search-parameters#_compartment) ```json { "resourceType": "AccessPolicy", "name": "Write Constraints Access Policy", "resource": [ { "resourceType": "Observation", "criteria": "Observation?_compartment=Patient/xyz" } ] } ``` #### Binaries The FHIR `Binary` resource is a special case. It is used to store arbitrary binary data, such as images or documents. Unlike other resources, `Binary` resources are not searchable, and therefore do not have search parameters, and therefore cannot use the `criteria` field in an `AccessPolicy`. To control access to `Binary` resources, you must use the `securityContext` field. This field is a reference to another resource, such as a `Patient`, `Practitioner`, or `Organization`, that defines the context in which the binary data is relevant. See [Binary Security Context](/docs/access/binary-security-context) for more information on how to use the `securityContext` field to control access to `Binary` resources. #### Parameterized Policies For more advanced access control configurations, You can use `%` variables to parameterize the access policy. Medplum provides the following built-in variables: | Variable | Description | | ------------- | -------------------------------------------------------------------------------------------- | | `%profile` | Reference to the current user's profile resource (e.g., `Practitioner/123` or `Patient/123`) | | `%profile.id` | The ID portion of the user's profile resource (e.g., `123`) |
Example: General Practitioner Access Policy The following access policy grants access to all [`Patient`](/docs/api/fhir/resources/patient) resources where the current user (a [`Practitioner`](/docs/api/fhir/resources/practitioner)) is listed as the general practitioner: ```json { "resourceType": "AccessPolicy", "name": "GP Access Policy", "resource": [ { "resourceType": "Patient", "criteria": "Patient?general-practitioner=%profile" } ] } ```
You can also set custom variables for access policies on the user's [ProjectMembership](/docs/api/fhir/medplum/projectmembership) resource. This policy then acts like a _template_, that can be instantiated, potentially multiple times, with different values for each variable.
Example: Custom Access Policy Variables In this example, the user with the parameterized policy shown above will only have access to Patient and DiagnosticReport resources, filtered by the relevant organizations. ```json { "resourceType": "AccessPolicy", "id": "123", "name": "Parameterized Access Policy", "resource": [ { "resourceType": "Patient", "criteria": "Patient?organization=%provider_organization" }, { "resourceType": "DiagnosticReport", "criteria": "DiagnosticReport?performer=%provider_organization" } ] } ``` We can set these variables on the user's [ProjectMembership](/docs/api/fhir/medplum/projectmembership) resource, as follows ```js { "resourceType": "ProjectMembership", "access": [ // Provide access to Patients and Diagnostic Reports in Organization/abc { "policy": { "reference": "AccessPolicy/123" }, "parameter": [ { "name": "provider_organization", "valueReference": { "reference": "Organization/abc" } } ] }, // Provide access to Patients and Diagnostic Reports in Organization/def { "policy": { "reference": "AccessPolicy/123" }, "parameter": [ { "name": "provider_organization", "valueReference": { "reference": "Organization/def" } } ] } ] } ```
Some common use cases for parameterized policies include: - Granting access to parents of pediatric patients - Granting clinicians access to patients in a specific state, based on the clinician's state license See this [video demo](https://www.youtube.com/watch?v=IDhsWiIxK3o) for an illustration. See [this Github Discussion](https://github.com/medplum/medplum/discussions/1453) for more examples of access scenarios that can be created using these policies. Parameterized policies are also the recommended pattern for temporary operational access, such as emergency patient access or temporary tenant access. #### Emergency Access Emergency access, sometimes called Break Glass access, is a controlled workflow for granting temporary access to protected health information when normal access paths are not sufficient. This pattern supports ONC criterion [d6 Emergency Access](/docs/compliance/onc#criteria-certified). When implementing emergency access, keep the grant narrow, time-bound, and auditable: - Require an explicit reason before access is granted. - Scope access to the smallest useful resource set, such as one patient or one tenant. - Prefer short-lived sessions or a temporary `ProjectMembership` entry instead of standing broad access. - Record who requested access, who approved it, the reason, the patient or tenant scope, and the revocation time. - Review `AuditEvent` records after the access window closes. For patient-specific escalation, one pattern is to add the support user or clinician's `Practitioner` profile to the patient's `generalPractitioner` list, then use an access policy scoped to that relationship: ```json { "resourceType": "AccessPolicy", "name": "Emergency Patient Access", "resource": [ { "resourceType": "Patient", "criteria": "Patient?general-practitioner=%profile" } ] } ``` After the emergency window, remove the temporary `generalPractitioner` reference and confirm that the user can no longer read the patient record. #### Temporary tenant access For support workflows, use a parameterized policy to grant access to one tenant for a limited operational window. Add a temporary `ProjectMembership.access` entry with the tenant reference as the parameter value, capture the reason and approval in your support system, and remove the entry when the work is complete. ```json { "resourceType": "ProjectMembership", "access": [ { "policy": { "reference": "AccessPolicy/tenant-support-readonly" }, "parameter": [ { "name": "organization", "valueReference": { "reference": "Organization/clinic-a" } } ] } ] } ``` #### Migrating from top-level compartments Older access policies may use the top-level `compartment` element to describe a broad compartment rule. For new policies, prefer resource-level `criteria` with parameterized values. This keeps each resource rule explicit and lets the same policy template be reused with different `ProjectMembership.access.parameter` values. For example, instead of relying on a top-level compartment rule, define the tenant filter on each resource that should be visible: ```json { "resourceType": "AccessPolicy", "name": "Organization Tenant Access", "resource": [ { "resourceType": "Patient", "criteria": "Patient?_compartment=%organization" }, { "resourceType": "Observation", "criteria": "Observation?_compartment=%organization" }, { "resourceType": "DiagnosticReport", "criteria": "DiagnosticReport?_compartment=%organization" } ] } ``` Then set the concrete tenant value on each membership: ```json { "resourceType": "ProjectMembership", "access": [ { "policy": { "reference": "AccessPolicy/organization-tenant-access" }, "parameter": [ { "name": "organization", "valueReference": { "reference": "Organization/clinic-a" } } ] } ] } ``` ### Example Access Policies #### Healthcare Partnerships A common need is to grant access to a subset of resources for a healthcare partnership. For example, a lab provider may want to grant access to all patient records that _originated from a specific client organization_. This can be achieved using Access Policy compartments. Assume that we have an Organization resource representing the customer: ```json { "resourceType": "Organization", "name": "Example Customer Organization", "id": "abc-123" } ``` This access policy grants read-only access to all Patients that are within that customer's "account" compartment: ```js { "resourceType": "AccessPolicy", "name": "Patient Example", // Any resource created will be tagged with `meta.account` set to `Organization/abc-123` "compartment": { "reference": "Organization/abc-123", "display": "Example Customer Organization" }, "resource": [ // Any read or search operation will filter on `meta.account` equals `Organization/abc-123` { "resourceType": "Patient", "criteria": "Patient?_compartment=Organization/abc-123" "readonly": true } ] } ``` The `meta.account` property is not FHIR standard. It is an extra `Reference` property in the `Meta` section. For example, here's how the Patient resource would look: ```json { "resourceType": "Patient", "id": "54aa8595-e3a7-48ae-af91-9c7cb940149b", "meta": { "versionId": "02900c57-4da8-498f-85d5-5077077e3e2c", "lastUpdated": "2022-01-13T16:21:11.870Z", "account": { "reference": "Organization/abc-123", "display": "Example Customer Organization" } }, "name": [ { "given": ["Homer"], "family": "Simpson" } ] } ``` Because the account-tagging is handled within the resource, project administrators and API users can set the account directly. #### Patient Access If you are building a patient-facing application (such as [FooMedical](https://github.com/medplum/foomedical)), a common requirement is to restrict each patient's access to only their own data. In this case it is recommended to use templated access policies, that also implement compartments as shown below. :::caution[Open Patient Registration] Patient Access is disabled by default. See our article on [enabling open patient registration](/docs/user-management/open-patient-registration) for instructions on enabling this functionality. ::: For an example Patient Access Policy template, see [your project's Default Access Policy](#default-access-policies). :::caution[Binary Security Context] `Binary` resources cannot use compartment-based access controls. Access is restricted by matching the `securityContext` field to the patient. When creating a `Binary` in a patient context, always set `securityContext` to the patient reference — otherwise the patient will not be able to access it: ```json { "resourceType": "Binary", "contentType": "application/pdf", "securityContext": { "reference": "Patient/homer-simpson" } } ``` See [Binary Security Context](/docs/access/binary-security-context) for more details. ::: You can configure your project to support open registration for patients, therefore it is crucial that you setup a Default Access Policy similar to the one above. #### Caregiver Access The [patient access policy](#patient-access) above can be combined with [policy parameterization](#parameterized-policies) to create an policy that allows caregivers to access data on behalf of patients (e.g parents on behalf of children. ```js { "resourceType": "ProjectMembership", "access": [ // Provide access to Patients and Diagnostic Reports in Organization/abc { "policy": { "reference": "AccessPolicy/patient-access-policy-template" }, "parameter": [ { "name": "patient", "valueReference": { "reference": "Patient/xyz" } } ] }, // Provide access to Patients and Diagnostic Reports in Organization/def { "policy": { "reference": "AccessPolicy/patient-access-policy-template" }, "parameter": [ { "name": "patient", "valueReference": { "reference": "Patient/uvw" } } ] } ] } ``` #### WebSocket Subscriptions (`useSubscription` Hook) To use the [`useSubscription`](/docs/react/use-subscription) hook, users need two things in their access policy: 1. **Permission to create `Subscription` resources** — The hook creates an in-memory `Subscription` on behalf of the user. Use a criteria scoped to websocket subscriptions owned by the current user so that users can only manage their own subscriptions: ``` Subscription?type=websocket&author=%profile ``` 2. **Read access to each subscribed resource type** — Users must also have `read` access for every resource type they intend to subscribe to. For example, if your app calls `useSubscription('Communication?...')`, the policy must grant at least `read` on `Communication`. ```json { "resourceType": "AccessPolicy", "name": "WebSocket Subscription Policy", "resource": [ { "resourceType": "Subscription", "criteria": "Subscription?type=websocket&author=%profile" }, { "resourceType": "Communication" } ] } ``` Add one entry per resource type your application subscribes to. #### Streamlined linkage and RBAC Control with AccessPolicy.basedOn In Medplum, users can attach one or more parameterized AccessPolicy resources to their ProjectMembership. During runtime, the Medplum server consolidates these resources into a single enforceable AccessPolicy. The recent enhancements introduce the **AccessPolicy.basedOn element**, which enhances traceability and linkage of access policies. These improvements are made accessible to the front end via the /auth/me endpoint, enabling seamless role-based access control (RBAC) for users. For more information, see the [PR](https://github.com/medplum/medplum/pull/4925) detailing the changes. This update allows for more granular control and visibility in RBAC implementations, where users have varying access levels based on their group or policy. - **Endpoint: /auth/me** - This endpoint provides access to the user's AccessPolicy information, including the basedOn element. - **AccessPolicy.basedOn** - This element is an array containing references to original AccessPolicy resources. - It provides traceability by linking the resolved AccessPolicy back to its source policies. - At runtime, Medplum resolves all nested and parameterized policies to build the basedOn array. - **Client-Side** - The introduction of AccessPolicy.basedOn allows for client-side changes based on the user's group/policy. - This is especially useful for Role-Based Access Control (RBAC) implementations where different users may have varying access levels. ### Default Access Policies New projects are created with a set of **default access policies**, one per profile type. These are stored on the [`Project.defaultAccessPolicies`](/docs/api/fhir/medplum/project) element, which maps a `profileType` to the `AccessPolicy` applied when a user of that type is invited without an explicit policy: | Profile type | Default policy grants | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `Practitioner` | Read/write access to all resource types, **except** read-only access to a curated set of knowledge, terminology, and conformance resource types. | | `Admin` | Full read/write access to all resource types (equivalent to having no access policy). | | `Patient` | Access scoped to the patient's own compartment. | | `RelatedPerson` | Access scoped to the associated patient's compartment, parameterized by `patient`. | When you [invite a user](/docs/app/invite) without specifying an access policy, Medplum applies the matching default for that profile type. Practitioners invited with the **admin** flag receive the `Admin` default; all other practitioners receive the `Practitioner` default. #### Switching a user to (or from) admin The `admin` flag does **not** bypass a user's access policy — it only grants access to project administration resource types (such as [`ProjectMembership`](/docs/api/fhir/medplum/projectmembership) and [`User`](/docs/api/fhir/medplum/user)) on top of the existing policy. This means that promoting a Practitioner to admin without also changing their policy would leave them "half-upgraded": still restricted by the read-only rules of the `Practitioner` default. To avoid this, Medplum automatically **reconciles the default access policy when the admin flag is toggled**: - Promoting a user to admin: if their current policy is exactly the project's `Practitioner` default, it is swapped to the `Admin` default. - Demoting an admin: if their current policy is exactly the project's `Admin` default, it is swapped back to the `Practitioner` default. This reconciliation only happens when the user's current policy is exactly the **opposite role's recognized default** — the `Practitioner` default when promoting, or the `Admin` default when demoting. Any other policy is left as-is. In particular, **custom (non-default) access policies are never modified**: if you have assigned a user a bespoke policy, toggling the admin flag leaves that policy untouched. :::tip[Custom access policy combinations] If a user needs a more specific combination of access than the role defaults provide, we recommend **overriding the default by supplying an explicit access policy when you invite (or edit) the user**. Assigning a custom policy both gives the user exactly the access they need and opts them out of the automatic admin/practitioner reconciliation described above, so their policy will not be swapped when the admin flag changes. ::: ### Related Resources - [Registration React Component](https://storybook.medplum.com/?path=/docs/medplum-registerform--basic) --- ## Admin Users Certain operations require Medplum [Users](/docs/api/fhir/medplum/user), [`Bots`](/docs/api/fhir/medplum/bot), or [`ClientApplications`](/docs/api/fhir/medplum/clientapplication) to have administrative privileges. Users can be granted admin rights on a per-project basis: a given user can be an admin for one project, but not another. Medplum distinguishes between two different types of admin user: **project admin** and **super admin**. ### Project Admin A project (or tenant) level user. This is the most common type of admin user at an organization. See our [User Management Guide](/docs/user-management#promote-existing-user-to-admin) for more information on how to grant project admin privileges. Project Admins have the following privileges: - **Invite / remove users** - See our [User Management Guide](/docs/user-management) for mor information - **Create Bots** - See our guide on [Bots](/docs/bots/bot-basics) to learn how to create and write Bots - **Write the `meta.account` element** - Every resource has a `meta.account` property that can be used for advanced access control scenarios. See our guide on [access policies](/docs/access/access-policies) for more information. - **View Administrative Resources** - The following Medplum resource types are only accessible to project admin [Users](/docs/api/fhir/medplum/user), [`Bots`](/docs/api/fhir/medplum/bot), or [`ClientApplications`](/docs/api/fhir/medplum/clientapplication): - [`Project`](/docs/api/fhir/medplum/project) - used to adjust [Project settings](/docs/access/projects#settings) - [`ProjectMembership`](/docs/api/fhir/medplum/projectmembership) - used to manage user registration and privileges (see: [User Management Guide](/docs/user-management)) - [`UserSecurityRequest`](/docs/api/fhir/medplum/usersecurityrequest) - used to [send custom emails](/docs/user-management/custom-emails#password-change-request-bot) - [`User`](/docs/api/fhir/medplum/user) - only for [project scoped users](/docs/user-management/project-vs-server-scoped-users#project-scoped-users) - **Impersonate users** - Project Admins are allowed to make API calls [on behalf of other users](/docs/auth/on-behalf-of) :::note[Applying Access Policies to Admins] If you want to limit these privileges, you can apply Access Policies to your Admin users. See the [Access Policies docs](/docs/access/access-policies) for more details. ::: ### Super Admin A super admin user has an increased level privileges for performing server-level operations. **This level of privilege can cause irreparable data changes, and should be limited to system administrators.** To grant a user super admin privilege, invite them to the Super Admin project (see [this guide](/docs/access/projects#superadmin) for more details). Project Admins have the following privileges: - **Overwrite all resource fields** - Super admin users bypass all data validation checks, and can edit protected fields like `id` and `meta` properties. - **Rebuild shared data structures** - Certain shared resources, such as `StructureDefinitions` and `ValueSets`, sometimes need to be built after some server updates. See the [Super Admin Guide](/docs/self-hosting/super-admin-guide) for more details. - **Create projects via API** - Because [`Projects`](/docs/api/fhir/medplum/project) are system-level resources, creating them via API requires the creation of a Super Admin [`ClientApplication`](/docs/api/fhir/medplum/clientapplication) --- ## Binary Security Context import ExampleCode from '!!raw-loader!@site/..//examples/src/binaries/create-media.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; FHIR Binary resources require special attention when implementing access controls in Medplum. Unlike other FHIR resources, Binary resources cannot use standard compartment-based access policies and must rely on the `securityContext` element for proper access control. **Critical Point**: Binary resources without a `securityContext` fall back to project-level access controls, potentially exposing sensitive data to any authenticated user with Binary resource permissions. ### Security Model #### Standard FHIR Resources Most FHIR resources (Patient, Observation, DiagnosticReport, etc.) can be secured using compartment-based access policies: ```json { "resourceType": "AccessPolicy", "resource": [ { "resourceType": "Patient", "criteria": "Patient?_compartment=%patient" } ] } ``` #### Binary Resources - Special Case Binary resources **cannot** use compartment-based access controls. They require explicit `securityContext` declaration: ```json { "resourceType": "Binary", "id": "example-binary", "contentType": "application/pdf", "securityContext": { "reference": "Patient/homer-simpson" } } ``` ### Risk Assessment #### Without securityContext - **Access Control**: Falls back to basic project permissions - **Risk Level**: Medium to High (depending on data sensitivity) - **Exposure**: Any authenticated user with `{ "resourceType": "Binary" }` permissions can access if they know the UUID - **UUID Security**: While 128-bit UUIDs provide practical obscurity, they can be exposed through: - Application logs - Network traffic - Browser history/developer tools - Error messages - Database exports - Accidentally shared URLs #### With securityContext - **Access Control**: Inherits permissions from referenced resource - **Risk Level**: Low (follows established access patterns) - **Exposure**: Only users with access to the referenced resource (e.g., Patient) can access the Binary ### Implementation Best Practices #### 1. HTTP API Binary Upload (Most Common) Most developers create Binary resources by uploading raw binary content via HTTP POST: ```bash # Recommended - Using X-Security-Context header curl -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/pdf" \ -H "X-Security-Context: Patient/homer-simpson" \ --data-binary "@patient-report.pdf" \ https://api.medplum.com/fhir/R4/Binary ``` ```bash # Not recommended - Missing security context curl -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/pdf" \ --data-binary "@patient-report.pdf" \ https://api.medplum.com/fhir/R4/Binary # This creates a Binary without securityContext! ``` The `X-Security-Context` header is the FHIR standard method for setting Binary security context during upload. #### 2. MedplumClient.createMedia (Recommended for SDK Users) **Best Practice**: Use `MedplumClient.createMedia()` when possible, as it automatically handles security context: {ExampleCode} Benefits of using `createMedia`: - Automatically creates both Media and Binary resources - Binary gets `securityContext` pointing to the Media resource - Media resource inherits compartment-based access controls - Provides better metadata and searchability than standalone Binary #### 3. Medplum React Components Medplum React components that handle file uploads automatically use secure patterns: ##### AttachmentButton Component ```jsx import { AttachmentButton } from '@medplum/react'; {(props) => ( )} ; ``` ##### AttachmentInput Component ```jsx import { AttachmentInput } from '@medplum/react'; { // Uploaded files automatically have proper securityContext console.log('New attachment:', attachment); }} />; ``` Both components use `MedplumClient.createMedia()` internally, ensuring proper security context. #### 4. Direct FHIR Resource Creation (Advanced) When creating Binary resources directly as FHIR resources (less common): ```javascript // Good - Always include securityContext const binary = await medplum.createResource({ resourceType: 'Binary', contentType: 'application/pdf', securityContext: { reference: `Patient/${patientId}`, }, }); // Bad - Missing securityContext const binary = await medplum.createResource({ resourceType: 'Binary', contentType: 'application/pdf', // Missing securityContext! }); ``` #### 5. Choose Appropriate Security Context | Use Case | Recommended securityContext | | ---------------------------------------- | ----------------------------------------------- | | Patient documents (lab results, reports) | `Patient/[patient-id]` | | Provider-specific content | `Practitioner/[practitioner-id]` | | Organization documents | `Organization/[org-id]` | | Encounter-specific files | `Encounter/[encounter-id]` | | Media-associated files | `Media/[media-id]` (automatic with createMedia) | #### 6. Bot Development Guidelines When creating Bots that generate Binary resources, prefer the HTTP API or createMedia approaches shown above. If creating Binary resources directly: ```javascript // Good - Always include securityContext const binary = await medplum.createResource({ resourceType: 'Binary', contentType: 'application/pdf', securityContext: { reference: `Patient/${patientId}`, }, }); // Better - Use createMedia instead const media = await medplum.createMedia({ data: pdfData, filename: 'report.pdf', contentType: 'application/pdf', subject: { reference: `Patient/${patientId}` }, }); ``` #### 7. DiagnosticReport Integration For DiagnosticReports with Binary attachments: ```json { "resourceType": "DiagnosticReport", "subject": { "reference": "Patient/homer-simpson" }, "presentedForm": [ { "contentType": "application/pdf", "url": "Binary/report-binary-id" } ] } ``` Ensure the Binary has matching securityContext: ```json { "resourceType": "Binary", "id": "report-binary-id", "contentType": "application/pdf", "securityContext": { "reference": "Patient/homer-simpson" } } ``` ### Audit and Remediation #### Finding Binary Resources Without securityContext Since Binary resources are not searchable, you can identify them through related resources: 1. **Through DiagnosticReports:** ``` GET /DiagnosticReport?_include=DiagnosticReport:presented-form ``` 2. **Through DocumentReferences:** ``` GET /DocumentReference?_include=DocumentReference:content ``` 3. **Through Media resources:** ``` GET /Media?_include=Media:content ``` #### Remediation Script Example ```javascript // Find DiagnosticReports and update associated Binaries const diagnosticReports = await medplum.searchResources('DiagnosticReport'); for (const report of diagnosticReports) { if (report.presentedForm) { for (const form of report.presentedForm) { if (form.url && form.url.includes('/Binary/')) { const binaryId = form.url.split('/Binary/')[1]; const binary = await medplum.readResource('Binary', binaryId); if (!binary.securityContext) { await medplum.updateResource({ ...binary, securityContext: { reference: report.subject.reference, }, }); } } } } } ``` For more details on how [`Binary`](/docs/api/fhir/resources/binary) resources are used in FHIR, see the [Binary Data docs](/docs/fhir-datastore/binary-data). Remember: **Every Binary resource containing sensitive data should have a securityContext**. When in doubt, err on the side of more restrictive access controls. --- ## Authorization and Access Control :::tip[Planning this workflow?] The [Access Control Decision Guide](/docs/decision-guides/access-control) walks through requirements questions and FHIR modeling decisions for access control — use it alongside these docs. ::: This section covers Medplum's _authorization_ tools. Medplum supports a rich set of primitives to provide fine-grained control over what data users can access and what operations they can perform. The key concept is the [`AccessPolicy`](/docs/api/fhir/medplum/accesspolicy) resource, which restricts read and write access to FHIR data on a per-resource-type or per-field basis. Every user or client application can be assigned an `AccessPolicy` that defines exactly which resources they can see and modify. - [Access Policies](/docs/access/access-policies) — Set up resource-level and field-level access control - [IP Address Rules](/docs/access/ip-access-rules) — Restrict access based on the user's IP address - [SMART Scopes](/docs/access/smart-scopes) — Support for [SMART-on-FHIR](https://docs.smarthealthit.org/) applications --- ## IP Access Rules One valuable way to prevent unauthorized data access is to restrict access to clinical data by the user's IP address. This article will discuss the benefits of IP address restrictions and provide a step-by-step guide on configuring these settings in Medplum's [AccessPolicy](/docs/api/fhir/medplum/accesspolicy). ### Importance of IP Address Restrictions IP address restrictions provide an additional layer of security by limiting access to your healthcare application based on the user's IP address. This ensures that only users from approved locations or networks can access the application, reducing the risk of unauthorized access or data breaches. Moreover, IP address restrictions can be an essential component of regulatory compliance for healthcare organizations, helping them adhere to strict data security requirements. Medplum customers primarily use IP Access Restrictions to ensure that users are only accessing data from approved origins such as on-premise devices or corporate VPN. ### Setting Up IP Address Restrictions in Medplum To configure IP address restrictions in Medplum, follow the steps below: 1. Log in to your Medplum account as a project administrator 2. Navigate to the AccessPolicy page (i.e., [https://app.medplum.com/AccessPolicy](https://app.medplum.com/AccessPolicy)) 3. Select an existing AccessPolicy or click "New..." to create a new one 4. Scroll down to the section labeled "IP Access Rules" 5. In the "IP Access Rules" section, you can add "allow" and "block" rules based on IP addresses The rules are evaluated sequentially until a matching rule is found. To effectively restrict access, start by specifying a series of "allow" rules for the desired IP addresses or IP address ranges. Once you have specified all the "allow" rules, add a wildcard "block" rule at the end to block all other IP addresses. To do this, use an asterisk (\*) as the value for the "block" rule. Please note that only IPv4 IP addresses are supported, and partial IP addresses can be used for matching. For example, specifying the value "8.8." would match "8.8.8.8" but would not match "8.7.8.8". --- ## Multi-Tenant Access Control import ExampleCode from '!!raw-loader!@site/..//examples/src/access/multi-tenant-access-policy.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ### Overview #### What is a tenant? In healthcare applications, Practitioners often work across multiple organizational boundaries. A doctor might work at multiple clinics, a nurse might be part of several care teams, or a care coordinator might manage patients across different healthcare services. Each of these logical groupings represents a distinct **tenant** in your system: a collection of FHIR resources (Patients, Observations, Encounters, etc.) that make sense to be grouped together. Using this model will allow you to restrict Users' access to only the resources that are part of the tenant(s) that you assign them to. This diagram shows how your User's access is determined by the tenant(s) that you assign them to and the Patients that belong to those tenants. ```mermaid %%{init: {'theme': 'neutral' }}%% flowchart TB subgraph Users["Users"] U1[User 1] U2[User 2] U3[User 3] end subgraph Tenants["Tenants"] T1[Tenant] T2[Tenant] end subgraph Patients["Patients"] P1[Patient A] P2[Patient B] P3[Patient C] P4[Patient D] subgraph CR1["Patient A's Resources"] Obs1[Observations] Comm1[Communications] QR1[QuestionnaireResponses] Aller1[AllergyIntolerances] MedReq1[MedicationRequests] end subgraph CR2["Patient B's Resources"] Obs2[Observations] Comm2[Communications] QR2[QuestionnaireResponses] Aller2[AllergyIntolerances] MedReq2[MedicationRequests] end subgraph CR3["Patient C's Resources"] Obs3[Observations] Comm3[Communications] QR3[QuestionnaireResponses] Aller3[AllergyIntolerances] MedReq3[MedicationRequests] end subgraph CR4["Patient D's Resources"] Obs4[Observations] Comm4[Communications] QR4[QuestionnaireResponses] Aller4[AllergyIntolerances] MedReq4[MedicationRequests] end P1 --> CR1 P2 --> CR2 P3 --> CR3 P4 --> CR4 end U1 --> T1 U2 --> T1 U2 --> T2 U3 --> T2 T1 --> P1 T1 --> P2 T2 --> P3 T2 --> P4 style Users fill:#e8f5e9 style Tenants fill:#e1f5ff style Patients fill:#fff4e1 style CR1 fill:#fce4ec style CR2 fill:#fce4ec style CR3 fill:#fce4ec style CR4 fill:#fce4ec ``` _User 1 can access Patient A and Patient B. User 2 can access all four Patients. User 3 can only access Patient C and Patient D._ This guide walks you through implementing multi-tenant access control in a single Medplum project. You will learn which questions you need to answer to build your tenancy model and how to implement it. Here is an overview of the steps you will take: 1. **Data Modeling**: How to represent tenants in FHIR using different resource types. 2. **Assigning data to tenants**: How to label your FHIR resources with the tenant(s) they belong to. 3. **User Registration & Management**: How to grant Users access to the tenant(s) they are enrolled in. ### Step 1: Modeling your tenants The first decision you need to make is which FHIR resource type semantically models your tenants. Common choices include: - **`Organization`**: Best for Managed Service Organizations (MSOs) where tenants represent different clinics, practices, or healthcare organizations. - **`HealthcareService`**: Best for service-based access control where tenants represent different departments or services (e.g., Cardiology Department, Oncology Department). - **`CareTeam`**: Best for care coordination scenarios where clinical data needs to be tightly contained to the providers working directly with a patient. Each patient gets their own `CareTeam` resource (e.g., `CareTeam/alice-smith`, `CareTeam/bob-jones`), and a clinician's `ProjectMembership` accumulates one access entry per patient as they are assigned to their care. See [Modeling Provider Organizations](/docs/administration/provider-directory/provider-organizations) for a deeper look at how CareTeams fit into your provider data model. **You are not limited to these resource types.** You can use any FHIR resource type that accurately models your tenants. The implementation patterns shown in the following steps apply regardless of your choice. Here's how the multi-tenancy model organizes resources when using different tenant types: **Organization** ```mermaid %%{init: {'theme': 'neutral' }}%% graph TB subgraph OrgSection[" "] UserOrg[User/user-1] PMOrg[ProjectMembership/membership-1] Org1[Organization/clinic-a] Org2[Organization/clinic-b] PatOrg1[Patient/patient-1] PatOrg2[Patient/patient-2] PatOrgShared[Patient/patient-3
Shared] UserOrg --> PMOrg PMOrg --> Org1 PMOrg --> Org2 Org1 --> PatOrg1 Org1 --> PatOrgShared Org2 --> PatOrg2 Org2 --> PatOrgShared end style UserOrg fill:#e8f5e9 style PMOrg fill:#fff4e1 style Org1 fill:#e1f5ff style Org2 fill:#e1f5ff style PatOrg1 fill:#e8f5e9 style PatOrg2 fill:#e8f5e9 style PatOrgShared fill:#fff4e1 ``` **HealthcareService** ```mermaid %%{init: {'theme': 'neutral' }}%% graph TB subgraph HSSection[" "] UserHS[User/user-1] PMHS[ProjectMembership/membership-1] HS1[HealthcareService/cardiology-service] HS2[HealthcareService/oncology-service] PatHS1[Patient/patient-1] PatHS2[Patient/patient-2] PatHSShared[Patient/patient-3
Shared] UserHS --> PMHS PMHS --> HS1 PMHS --> HS2 HS1 --> PatHS1 HS1 --> PatHSShared HS2 --> PatHS2 HS2 --> PatHSShared end style UserHS fill:#e8f5e9 style PMHS fill:#fff4e1 style HS1 fill:#e1f5ff style HS2 fill:#e1f5ff style PatHS1 fill:#e8f5e9 style PatHS2 fill:#e8f5e9 style PatHSShared fill:#fff4e1 ``` **CareTeam** ```mermaid %%{init: {'theme': 'neutral' }}%% graph TB subgraph CTSection[" "] UserCT[User/nurse-jane] PMCT[ProjectMembership/membership-1] CT1[CareTeam/alice-smith] CT2[CareTeam/bob-jones] PatCT1[Patient/alice-smith] PatCT2[Patient/bob-jones] UserCT --> PMCT PMCT --> CT1 PMCT --> CT2 CT1 --> PatCT1 CT2 --> PatCT2 end style UserCT fill:#e8f5e9 style PMCT fill:#fff4e1 style CT1 fill:#e1f5ff style CT2 fill:#e1f5ff style PatCT1 fill:#e8f5e9 style PatCT2 fill:#e8f5e9 ``` _Each patient has their own CareTeam. Nurse Jane's ProjectMembership accumulates one access entry per patient as she is assigned to their care._ Once you've chosen your tenant resource type, use it consistently throughout your implementation. ### Step 2: Assigning data to tenants Once you've decided on your tenant resource type and created your tenant resources, you need to associate your data (patients, observations, encounters, etc.) with those tenants. This is done using compartments and the `$set-accounts` operation. Most implementations will be able to just assign Patients to tenants. This is because Patient is the only resource where _"propagation"_ is supported to the Patient's _"related"_ resources. See [Why is Patient a special case?](#why-is-patient-a-special-case) for more details. :::info[] If your Patient is also a User that can log in, this will not impact what that Patient User can access. Instead, this impacts who can access that Patient. :::
Example Method: Assigning a Patient to an Organization Tenant {ExampleCode}
Find other example enrollment methods [here](https://github.com/medplum/medplum-mso-demo/blob/main/src/utils/enrollment.ts). #### Understanding Compartments Compartments are an advanced FHIR concept that gives you a way to label a resource with a reference to a tenant that the resource belongs to. For example, this would be the basic structure of a Patient that belongs to a tenant **(Organization, HealthcareService, or CareTeam)**: **Organization** ```json //This Patient belongs to the clinic-a tenant { "resourceType": "Patient", "meta": { "compartment": [{ "reference": "Organization/clinic-a" }] } //... } ``` **HealthcareService** ```json //This Patient belongs to the cardiology-service tenant { "resourceType": "Patient", "meta": { "compartment": [ { "reference": "HealthcareService/cardiology-service" } ] }, //... } ``` **CareTeam** ```json //This Patient belongs to the diabetes-care-team tenant { "resourceType": "Patient", "meta": { "compartment": [{ "reference": "CareTeam/diabetes-care-team" }] } //... } ``` #### Adding references to the meta.compartment field In all resources, the `meta.compartment` field is **readonly**. You cannot modify it directly. Instead, you need to use the [$set-accounts](/docs/api/fhir/operations/set-accounts) operation to add references to the **meta.compartment** field. **Organization** {ExampleCode} This will update the **meta.compartment** field of the Patient resource to include the reference to the Organization. Here is what the updated Patient resource will look like: ```json { "resourceType": "Patient", "meta": { "compartment": [{ "reference": "Organization/clinic-a" }] } //... } ``` **HealthcareService** {ExampleCode} This will update the **meta.compartment** field of the Patient resource to include the reference to the HealthcareService. Here is what the updated Patient resource will look like: ```json { "resourceType": "Patient", "meta": { "compartment": [{ "reference": "HealthcareService/cardiology-service" }] } //... } ``` **CareTeam** {ExampleCode} This will update the **meta.compartment** field of the Patient resource to include the reference to the CareTeam. Here is what the updated Patient resource will look like: ```json { "resourceType": "Patient", "meta": { "compartment": [{ "reference": "CareTeam/diabetes-care-team" }] } //... } ``` Any resource can be assigned to a tenant with this model. For example, you could also assign a [Questionnaire](/docs/api/fhir/resources/questionnaire) to a tenant by calling the [$set-accounts](/docs/api/fhir/operations/set-accounts) operation on that specific resource. **Organization** {ExampleCode} **HealthcareService** {ExampleCode} **CareTeam** {ExampleCode} #### Why is Patient a special case? **Patient is unique because it is the only resource where propagation is supported**. This means that you can assign a Patient to a tenant and all resources that _"belong"_ to that Patient will automatically be assigned to the same tenant as well. **A resource is determined to _"belong"_ to a Patient based on the resource relationships defined by the [Patient CompartmentDefinition](https://hl7.org/fhir/R4/compartmentdefinition-patient.html)**. For example, a [QuestionnaireResponse](/docs/api/fhir/resources/questionnaireresponse) belongs to a Patient if it has a reference to the Patient in the `subject` or `author` field, a [Communication](/docs/api/fhir/resources/communication) belongs to a Patient if it has a reference to the Patient in the `subject`, `recipient`, or `sender` field, etc. **To assign a Patient and all resources that _"belong"_ to that Patient to a tenant, call the [$set-accounts](/docs/api/fhir/operations/set-accounts) operation with `propagate` set to `true`.** **Organization** {ExampleCode} **HealthcareService** {ExampleCode} **CareTeam** {ExampleCode} **You only need to call the [$set-accounts](/docs/api/fhir/operations/set-accounts) operation once when assigning Patient to a tenant.** You do not need to call it again when new resources are created that _"belong"_ to that Patient because they will inherit the `meta.compartment` field from the Patient. #### What if you need to assign a Patient to multiple tenants? This is common in scenarios where you need the Patient to be visible across multiple organizations, services, or care teams. To assign a Patient to multiple tenants, you can call the [$set-accounts](/docs/api/fhir/operations/set-accounts) with multiple tenant references. This will label the Patient and all resources that _"belong"_ to that Patient with the multiple tenant references. **Organization** {ExampleCode} **HealthcareService** {ExampleCode} **CareTeam** {ExampleCode} ### Step 3: User Registration & Management Once you've modeled your tenants and associated data with them, you also need to give your Practitioner Users access to those tenants. This is done through [AccessPolicies](/docs/access/access-policies) and [ProjectMembership](/docs/api/fhir/medplum/projectmembership) configuration. More specifically, it leverages [Parameterized Access Policies](/docs/access/access-policies#parameterized-policies) to pass through the tenant references as parameters into the AccessPolicy. #### Create AccessPolicies with Parameterized Variables Your AccessPolicy uses parameterized variables (like `%organization`, `%healthcare_service`, or `%care_team`) that get replaced at runtime with the tenant references from the user's ProjectMembership. These variables are used in the [compartment](/docs/access/access-policies#compartments) section and for [Criteria-based Access Control](/docs/access/access-policies#compartments). For new multi-tenant policies, prefer resource-level `criteria` with parameterized values instead of legacy top-level `AccessPolicy.compartment` rules. This makes each resource rule explicit and lets one policy template be reused across many `ProjectMembership.access` entries. In your design, you should decide which resource types you want to restrict access to within your tenants versus which resource types you want to allow access to. **Organization** {ExampleCode} **HealthcareService** {ExampleCode} **CareTeam** {ExampleCode} #### Configure ProjectMemberships to Reference Tenants The User's `ProjectMembership` references their enrolled tenants via the `access.parameter` array. Each parameter name must match the variable name used in the `AccessPolicy` (e.g., if your AccessPolicy uses `%organization`, the User's ProjectMembership `access.parameter` should be named `"organization"`). **Organization** ```json { "resourceType": "ProjectMembership", "access": [ { "policy": { "reference": "AccessPolicy/mso-policy" }, "parameter": [ { "name": "organization", "valueReference": { "reference": "Organization/clinic-a" } } ] } ] } ``` **HealthcareService** ```json { "resourceType": "ProjectMembership", "access": [ { "policy": { "reference": "AccessPolicy/service-policy" }, "parameter": [ { "name": "healthcare_service", "valueReference": { "reference": "HealthcareService/cardiology-service" } } ] } ] } ``` **CareTeam** ```json { "resourceType": "ProjectMembership", "access": [ { "policy": { "reference": "AccessPolicy/careteam-policy" }, "parameter": [ { "name": "care_team", "valueReference": { "reference": "CareTeam/diabetes-care-team" } } ] } ] } ``` #### When should you assign a User to a tenant? You can assign a User to a tenant at **invite time** or **after they have been invited**. ##### Invite `Practitioner` Users at Invite Time Invite `Practitioner` Users associated with their respective tenants via the [`/admin/invite` endpoint](/docs/api/project-admin/invite):
Example: Inviting a Practitioner User with Tenant Access {ExampleCode}
##### Assign `Practitioner` Users to a Tenant After Invite Here is an example method for updating a User's ProjectMembership to add a new tenant after the User already exists:
Example Method: Enrolling a Practitioner in an Organization Tenant After Invite {ExampleCode}
#### What if your User needs to access multiple tenants? To grant access to multiple tenants, include multiple entries in the `access` array, each with different tenant parameters. **This pattern scales to any number of tenants per user.** For example, a nurse with a panel of 100 students would have 100 entries in their `access` array — one per student's CareTeam — and this is expected and supported. **Organization** ```json { "resourceType": "ProjectMembership", "access": [ { "policy": { "reference": "AccessPolicy/mso-policy" }, "parameter": [ { "name": "organization", "valueReference": { "reference": "Organization/clinic-a" } } ] }, { "policy": { "reference": "AccessPolicy/mso-policy" }, "parameter": [ { "name": "organization", "valueReference": { "reference": "Organization/clinic-b" } } ] } ] } ``` **HealthcareService** ```json { "resourceType": "ProjectMembership", "access": [ { "policy": { "reference": "AccessPolicy/service-policy" }, "parameter": [ { "name": "healthcare_service", "valueReference": { "reference": "HealthcareService/cardiology-service" } } ] }, { "policy": { "reference": "AccessPolicy/service-policy" }, "parameter": [ { "name": "healthcare_service", "valueReference": { "reference": "HealthcareService/neurology-service" } } ] } ] } ``` **CareTeam** ```json { "resourceType": "ProjectMembership", "access": [ { "policy": { "reference": "AccessPolicy/careteam-policy" }, "parameter": [ { "name": "care_team", "valueReference": { "reference": "CareTeam/alice-smith" } } ] }, { "policy": { "reference": "AccessPolicy/careteam-policy" }, "parameter": [ { "name": "care_team", "valueReference": { "reference": "CareTeam/bob-jones" } } ] } // ... one entry per patient CareTeam the user is assigned to ] } ``` #### Can some resource types still be accessed by Users, regardless of the tenant(s) they are assigned to? A good thing to consider is that your application may need to allow some resource types to be accessed by Users, regardless of the tenant(s) they are assigned to. For example, you may want to allow Questionnaires to be accessed by Users, regardless of the tenant(s) they are assigned to because you want one single Patient Onboarding Questionnaire or you want Practitioners to be able to view each other's profiles, even if they are not assigned to the same tenant. To do this, do not include the `_compartment` check in resource type criteria for the resource types you want to allow access to. {ExampleCode} ### Optional: Building a Tenant Selector After you’ve granted a user access to multiple tenants, most MSO apps also need a way to decide **which tenant context the user is working in right now** (e.g., “Clinic A” vs “Clinic B”), while still supporting “cross-tenant” workflows like consolidated scheduling. See [Building a Tenant Selector](/docs/access/tenant-selector). ### Conclusion Multi-tenant access control in Medplum enables you to securely partition healthcare data within a single project by leveraging FHIR compartments and parameterized access policies. The implementation follows three key steps: 1. **Model your tenants** using appropriate FHIR resource types (Organization, HealthcareService, CareTeam, or others) that accurately represent your organizational structure. 2. **Assign data to tenants** using the `$set-accounts` operation to populate the `meta.compartment` field, which labels the resource with the tenant(s) it belongs to. Remember that Patient resources support propagation, automatically assigning related resources to the same tenant(s). 3. **Grant user access** through ProjectMembership configuration, using parameterized AccessPolicies that reference tenant compartments to restrict access appropriately. By following these patterns, you can create flexible access control models that support complex healthcare workflows while maintaining data isolation between tenants. Providers can work across multiple tenants and patients can be shared between tenants. For more advanced access control scenarios, see [Building Multi-Tenant MSO with Medplum](/blog/multi-tenant-mso) and the [Access Policies documentation](/docs/access/access-policies).
Example LLM Prompt: Recommending Tenanting Resources and Access Policies Answer the questions in the below prompt, and copy and paste it into an LLM. {ExampleCode}
--- ## Project-Scoped URLs Medplum serves its API under project-scoped base URLs in addition to the standard root URL. A project-scoped URL names the target [Project](/docs/access/projects) in the path, and Medplum verifies that every authenticated request made through it belongs to that project. Project-scoped URLs give each tenant its own API endpoint, OAuth issuer, and discovery documents. They also add a layer of tenant isolation: credentials issued for one project's endpoint cannot be used against another project's endpoint. ### URL Format Every API route is available under two project-scoped prefixes: ```text https://api.medplum.com/projects/{projectId}/... https://api.medplum.com/api/projects/{projectId}/... ``` For example, the FHIR endpoint for a project is: ```text https://api.medplum.com/projects/{projectId}/fhir/R4/ ``` Project-scoped URLs cover the full API surface, including FHIR, Binary, asynchronous batch, OAuth, DICOMweb, and subscription WebSockets. The root URLs (`https://api.medplum.com/...` and `https://api.medplum.com/api/...`) remain available and unchanged. ### Security Model #### The URL asserts the project, the credential decides it The project ID in the URL is an assertion that Medplum checks. It is never a source of identity or access. When an authenticated request arrives on a project-scoped URL, Medplum resolves the caller's identity exactly as it does on the root URL: from the credential to the `Login`, then to the `ProjectMembership`, then to the `Project`. Medplum then compares that project to the project ID in the URL. If they differ, the request is rejected with `403 Forbidden`. The project ID in the URL does not: - Grant access to the project - Select a membership for a user who belongs to several projects - Change which repository or data the request reads and writes - Bypass or relax [Access Policies](/docs/access/access-policies), [IP Access Rules](/docs/access/ip-access-rules), or [SMART scopes](/docs/access/smart-scopes) Project IDs are identifiers, not secrets. Knowing a project ID gives a caller nothing without a valid credential for that project. #### Tokens are bound to the URL scope that issued them Each project-scoped URL acts as its own OAuth issuer. The issuer for a project is: ```text https://api.medplum.com/projects/{projectId}/ ``` When a client obtains tokens through a project-scoped token endpoint, Medplum sets both the `iss` and `aud` claims of the access token to the project-scoped issuer. When a request arrives, Medplum verifies the token against the issuer implied by the request URL. The same applies to refresh tokens, token introspection, and `id_token_hint` during authorization. As a result, a token is only accepted under the URL scope where it was issued: | Token issued through | Used at root URL | Used at project A URL | Used at project B URL | | ------------------------ | ---------------- | --------------------- | --------------------- | | Root token endpoint | Accepted | Rejected | Rejected | | Project A token endpoint | Rejected | Accepted | Rejected | A leaked or misrouted project-scoped token cannot be replayed against the root API or against another project's endpoint. The `/api` and non-`/api` forms of the same project URL share an issuer, so a token works under both. #### Client assertions are audienced to the project [Client assertion](/docs/auth/client-assertion) authentication (private key JWT) requires the `aud` claim to match the token endpoint URL. On a project-scoped URL, that is the project-scoped token endpoint: ```text https://api.medplum.com/projects/{projectId}/oauth2/token ``` An assertion created for one project's token endpoint is rejected by the root token endpoint and by every other project's token endpoint. #### Discovery stays within the project scope Discovery documents fetched from a project-scoped URL advertise only project-scoped endpoints. A client that begins discovery at a project URL is never redirected to the root issuer or to another project. | Document | Project-scoped path | | ---------------------------- | ------------------------------------------------------------ | | OpenID Connect configuration | `/projects/{projectId}/.well-known/openid-configuration` | | SMART configuration | `/projects/{projectId}/.well-known/smart-configuration` | | OAuth protected resource | `/projects/{projectId}/.well-known/oauth-protected-resource` | The OpenID configuration advertises project-scoped `issuer`, `authorization_endpoint`, `token_endpoint`, `userinfo_endpoint`, `introspection_endpoint`, `registration_endpoint`, and `jwks_uri` values. #### Subscription WebSockets enforce the project A subscription WebSocket opened on a project-scoped URL must bind to a subscription in the same project. If the subscription belongs to a different project, Medplum sends a `403 Forbidden` outcome and does not bind the subscription. #### External JWTs can be scoped to a project Some external systems issue JWTs that identify the issuer but carry no Medplum project, user, or profile. On a project-scoped URL, Medplum can accept these tokens directly as bearer tokens by matching the token issuer to a `ClientApplication` in that project. This lookup only runs on project-scoped URLs and is designed to fail closed: - The JWT signature is verified against the `jwksUrl` configured on the client's identity provider. - The JWT `iss` claim must match the identity provider's configured `issuer`. - The client lookup is constrained to the project named in the URL. - Exactly one `ClientApplication` in the project must match the issuer. Zero or multiple matches are rejected. - The request authenticates as the client's existing `ProjectMembership`. A token never creates a membership or grants access by itself. Server-wide `externalAuthProviders` are checked first, as described in [Direct External Authentication](/docs/auth/direct-external-auth). ### What Project-Scoped URLs Do Not Do Project-scoped URLs strengthen isolation for clients that use them. They are not a project-level access control on their own. - **Root URLs still work.** A client with valid credentials for a project can still call the root API. Project-scoped URLs do not force a project's traffic through its scoped endpoint. - **Sign-in and token issuance are not restricted by the URL.** Login, registration, and the token endpoint behave as they do on the root URL, and do not check the project in the URL. A user or client from project B can obtain tokens through project A's endpoint, but every API request rejects those tokens. They fail the project check with `403 Forbidden` on project A's URL, and fail issuer verification with `401 Unauthorized` everywhere else. - **Access control is unchanged.** Access Policies, SMART scopes, and IP Access Rules apply exactly as they do on the root URL. ### When to Use Project-Scoped URLs Use project-scoped URLs when: - **An integration needs a distinct endpoint per tenant.** ONC endpoint discovery and CMS-0057-F Coverage Requirements Discovery (CRD) testing both expect a tenant-specific base URL. - **An external system's credentials do not identify the project.** Backend-service credentials and external JWTs that carry no Medplum identity need the URL to route them to the correct project. - **You want defense in depth for multi-tenant integrations.** Binding tokens to a project issuer keeps a misconfigured or compromised client from reaching data outside its intended project. ### Using Project-Scoped URLs with the Medplum SDK Set the client's `baseUrl` to the project-scoped URL. The client derives its FHIR, OAuth, and WebSocket URLs from the base URL, so every request uses the project scope. ```ts import { MedplumClient } from '@medplum/core'; const medplum = new MedplumClient({ baseUrl: 'https://api.medplum.com/projects/YOUR_PROJECT_ID/', }); await medplum.startClientLogin(MY_CLIENT_ID, MY_CLIENT_SECRET); ``` Tokens obtained this way use the project-scoped issuer. Keep the same `baseUrl` for every request made with those tokens, since the root URL rejects them. ### Availability Project-scoped URLs are available in Medplum v5.1.30 and later. DICOMweb and subscription WebSocket support was added in v5.1.38. No configuration is required. --- ## Projects Medplum [`Projects`](/docs/api/fhir/medplum/project) are the primary mechanism of access control. [`Projects`](/docs/api/fhir/medplum/project) are isolated containers of FHIR resources that are administered separately, and which can have different settings. A common requirement for development teams is to have a separate [`Project`](/docs/api/fhir/medplum/project) , with non-protected data, for testing and debugging, before deploying workflow changes to production. A common Medplum usage pattern is to create a "development", "staging", and "production" [`Project`](/docs/api/fhir/medplum/project). ### Isolation Model Medplum [`Projects`](/docs/api/fhir/medplum/project) create a hard boundary between FHIR resources, and resources within one project cannot reference resources in another. Additionally, [`Projects`](/docs/api/fhir/medplum/project) each have their own user administration. A user can be a member of one, or multiple [`Projects`](/docs/api/fhir/medplum/project), with different privileges in each. See our [User Administration Guide](/docs/user-management) for more information. [`Projects`](/docs/api/fhir/medplum/project) can each be configured with own global settings and secrets (see [Project Settings](#settings) below). ### Project Linking Sometimes it is useful to share a common set of resources with multiple projects. Medplum super administrators can create shared projects and _link_ them into multiple target projects. Users of those target projects get a _read-only_ view of all resources in the shared projects. When a project is linked, all resources from the linked project appear alongside the target project's resources in search results and queries. #### Common Use Cases - Sharing large [`CodeSystems`](/docs/api/fhir/resources/codesystem) and [`ValueSets`](/docs/api/fhir/resources/valueset) for standard terminology. For example the [Medplum UMLS integration](/pricing): [ICD-10](/docs/charting/chart-data-model#diagnoses-and-problem-list), [RxNORM](/docs/medications/medication-codes#rxnorm), [LOINC](/docs/careplans/loinc), SNOMED - Sharing [FHIR profiles](/docs/fhir-datastore/profiles) (`StructureDefinition` resources) for a specific clinical domain - Sharing common data sets (e.g. Medplum Payor Directory, Medplum Lab Directory) - Sharing [Bots](/docs/bots) Certain Medplum features, including first-party integrations, require access to shared sets of resources, such as [`CodeSystem`](/docs/api/fhir/resources/codesystem), [`ValueSet`](/docs/api/fhir/resources/valueset), and [`Organization`](/docs/api/fhir/resources/organization). #### Setting Up a Shared Project Configuring project linking is a two-step process that requires super-admin access: **Step 1: Create the shared project** Create a new [`Project`](/docs/api/fhir/medplum/project) that will hold the shared resources. This project will not be used directly by end users — it exists solely as a container for shared content. **Always set `exportedResourceType`** on the shared project to restrict which resource types are visible to linked projects. If `exportedResourceType` is omitted, **all resource types in the shared project are accessible**. ```json { "resourceType": "Project", "name": "Shared Terminology", "exportedResourceType": ["CodeSystem", "ValueSet"] } ``` **Step 2: Link the shared project into target projects** Add a `link` entry to each target [`Project`](/docs/api/fhir/medplum/project) that should have access to the shared resources. This must be done by a super admin. ```json { "resourceType": "Project", "name": "My App - Production", "link": [ { "project": { "reference": "Project/" } } ] } ``` A project can link to multiple shared projects by adding multiple entries to the `link` array. #### Access Control for Linked Projects - **Read-only**: Users in a target project can read and search resources from linked projects, but cannot create, update, or delete them. - **Transparent**: Linked resources appear seamlessly in search results alongside local resources — no special handling is required in queries. - **Scoped by `exportedResourceType`**: If the shared project specifies `exportedResourceType`, only those resource types are visible to linked projects. If `exportedResourceType` is empty or omitted, all resource types are exported. - **Project-admin resources are excluded**: Administrative resource types (e.g., `ProjectMembership`, `ClientApplication`) from a linked project are never visible to users in the target project, regardless of `exportedResourceType`. #### Viewing Linked Projects You can see linked Projects in the Medplum App by: - Navigating to [app.medplum.com/Project](https://app.medplum.com/Project) - Selecting your Project - Selecting the "Details" tab #### Distinguishing Local vs. Linked Resources Because linked resources are returned alongside local resources in search results, you may need to tell them apart. The `_compartment` search parameter filters results to resources that belong to a specific project compartment: ```typescript // Search only within the current project (exclude linked projects) const localOnly = await medplum.searchResources('ValueSet', { _compartment: 'Project/', }); // Search only within the shared project const sharedOnly = await medplum.searchResources('ValueSet', { _compartment: 'Project/', }); ``` You can also inspect `resource.meta.project` on any returned resource to determine which project it belongs to. #### Best Practices :::warning[Always set `exportedResourceType`] **Always specify `exportedResourceType` on shared projects.** If this field is omitted, _every_ resource type in the shared project is visible to all linked projects — including any resource types added in the future. This is an easy source of unintended data exposure. Define the exact set of resource types that linked projects need and nothing more: ```json { "resourceType": "Project", "name": "Shared Terminology", "exportedResourceType": ["CodeSystem", "ValueSet"] } ``` If you later add a new resource type to the shared project (e.g. `Organization` for a payor directory), update `exportedResourceType` explicitly rather than removing the restriction. ::: - **Principle of least privilege**: treat `exportedResourceType` the same way you would treat access policy resource restrictions — grant access only to what is needed, and review the list whenever the shared project's contents change. - **Separate shared projects by domain**: rather than one large shared project with a broad `exportedResourceType` list, consider splitting into focused projects (e.g. one for terminology, one for profiles). This makes it easier to link only the relevant project into each target and limits blast radius if a project is misconfigured. - Populate shared projects with read-only reference data (terminology, profiles, directories) rather than patient or clinical data. - Be aware that queries like `medplum.searchResources()` will return the first matching resource across all accessible projects (both local and linked). Use `_compartment` if you need deterministic project-scoped results. ### The SuperAdmin `Project` The main exception to this isolation model is the "Super Admin" project. This is a special project that provides a global view over all the resources on the Medplum server. See our [SuperAdmin Guide](/docs/self-hosting/super-admin-guide) for more information. The SuperAdmin has the following privileges: - Access to protected resources - Ability to overwrite the `id` of a resource, which is normally server generated - Ability to overwrite fields in the `meta` element of resources such as `author`, `lastUpdated`, etc. :::warning[] Logging into the Super Admin project allows for potentially dangerous operations and is only intended for server administrators ::: :::note[Checking If You Are In The SuperAdmin Project] To switch to the SuperAdmin project or check if you are already in it, you can use the [**profile selector**](/docs/app/app-introduction/index.md#profile-selector). ![project switcher](project-switcher.png) ::: ### Creating a Project ##### Medplum App - Visit the https://app.medplum.com/register or visit https://app.medplum.com/signin and click the "Register" link - Sign in with an existing user, or enter the details for a new user account - Enter your project name ### Project Settings Project-level settings can be used to configure server behavior for different groups of users. A subset of the available settings related to authentication and access control are shown below; see the full [Project Settings](/docs/self-hosting/project-settings) documentation for more information. | Setting | Description | Default | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | | `superAdmin` | Whether this project is the super administrator project ([see above](#superadmin)). | `false` | | `features` | A list of optional features that are enabled for the project. Values related to access control include: `bots`: This [`Project`](/docs/api/fhir/medplum/project) is allowed to create and run [Bots](/docs/bots/bot-basics). `email`: Bots in this project can [send emails](/docs/sdk/core.medplumclient.sendemail). `cron`: This [`Project`](/docs/api/fhir/medplum/project) can run Bots on [CRON timers](/docs/bots/bot-cron-job) `google-auth-required`: [Google authentication](/docs/auth/google-auth) is the only method allowed for this [`Project`](/docs/api/fhir/medplum/project) | | | `defaultPatientAccessPolicy` | The default [`AccessPolicy`](/docs/access/access-policies) applied to all [Patient Users](/docs/user-management/project-vs-server-scoped-users#project-scoped-users) invited to this [`Project`](/docs/api/fhir/medplum/project). This is required to enable [open patient registration](/docs/user-management/open-patient-registration). | | ### Default Profiles The `defaultProfile` setting automatically applies [FHIR profiles](http://hl7.org/fhir/R4/profiling.html#resources) to resources that do not specify a profile in `meta.profile`. This is useful for enforcing consistent validation and clinical data quality across a project without requiring every resource creation call to include profile references. #### How It Works When a resource is created or updated **without** a `meta.profile` field, the server checks the project's `defaultProfile` configuration. If a matching entry exists for the resource type, the server automatically adds the configured profile(s) to the resource's `meta.profile` array before validation. If the resource **already** has a `meta.profile` field, default profiles are **not** applied — the resource's explicit profile is preserved. #### Configuration Set `defaultProfile` on a `Project` resource as an array of objects, each specifying a `resourceType` and the `profile` URLs to apply: ```json { "resourceType": "Project", "name": "My Clinical App", "defaultProfile": [ { "resourceType": "Observation", "profile": [ "http://hl7.org/fhir/StructureDefinition/vitalsigns" ] }, { "resourceType": "Patient", "profile": [ "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient" ] } ] } ``` #### Example: Enforcing Vital Signs Validation With the configuration above, any `Observation` created without an explicit profile will automatically receive the [Vital Signs profile](https://hl7.org/fhir/R4/observation-vitalsigns.html). This means the server will validate that the observation includes required elements like `subject`, `category`, and the correct value types: ```typescript // This observation will automatically get the vital-signs profile applied const observation = await medplum.createResource({ resourceType: 'Observation', status: 'final', code: { text: 'Heart rate' }, effectiveDateTime: '2024-01-01T00:00:00Z', valueInteger: 72, // No meta.profile specified — defaultProfile kicks in }); // observation.meta.profile will be: // ["http://hl7.org/fhir/StructureDefinition/vitalsigns"] ``` If the observation is missing required fields (e.g., `subject`), the server will reject it with a validation error — even though no profile was explicitly specified. ### Project Secrets Each [`Project`](/docs/api/fhir/medplum/project) can store a set of key/value pairs to store configuration values, such as API keys, needed by Bots. See [Bot Secrets](/docs/bots/bot-secrets) for more information. ### Cloning and Expunging `Projects` Self-hosted users have two advanced project administration operations available to them: - `$clone` - Make a copy of an existing [`Project`](/docs/api/fhir/medplum/project) and all its resources. - `$expunge` - Perform a "hard delete" of [`Project`](/docs/api/fhir/medplum/project) and all its resources. This will remove all the related resource rows from the database. For more information, refer to the Super Admin [Project Management guide](/docs/self-hosting/super-admin-cli#project-management) ### See Also - [User management guide](/docs/user-management) - [Super Admin Guide](/docs/self-hosting/super-admin-guide) - [Super Admin CLI](/docs/self-hosting/super-admin-cli#project-management) - [Project Resource Schema](/docs/api/fhir/medplum/project) --- ## SMART Scopes SMART on FHIR’s authorization scheme uses [OAuth2 scopes](https://oauth.net/2/scope/) to communicate (and negotiate) access requirements. For those using scopes, we recommend reading [background information on HL7.org](http://hl7.org/fhir/smart-app-launch/scopes-and-launch-context.html). For background this documentation on [OAuth scopes](https://auth0.com/docs/get-started/apis/scopes) is also useful. Medplum supports the `SMART App Launch 2.0.0` [standard](https://hl7.org/fhir/smart-app-launch/). ### When and Why SMART Scopes are Useful SMART Scopes are best understood as part of a FHIR-enabled app ecosystem. Imagine a patient portal containing FHIR data, with a patient logged in. Within that portal is a link to another application, and when that link is clicked the second application is launched - prompting the user to authorize and grant access to the data in the patient portal. This handshake is common in and supported by EHRs, PHRs and other systems that store and use FHIR data. SMART Scopes provide a secure authorization protocol for a variety of app architectures, including apps that run on an end-user’s device (e.g. mobile devices) as well as apps that run on a secure server. The framework `SMART App Launch` supports four key use cases: - Patients apps that launch standalone - Patient apps that launch from a portal like a personal health record - Provider apps that launch standalone - Provider apps that launch from a portal like a traditional electronic health record ### Supported Scopes Medplum supports the following scopes. - `patient/*.rs` - `user/*.cruds` - `openid` - `fhirUser` - `launch` - `launch/patient` - `offline_access` - `online_access` ### Understanding Scopes Scopes grant read, write, search and update access to specific resources, here are some examples of scopes and what they grant access to. - `scope=patient/Observation.r`: the user is allowed to read Observation resources for a specific patient - `scope=user/Encounter.cu`: the user is allowed to write (create and update) Encounter resources - `scope=user/*.rs`: the user is allowed to read and search any type of resource - `scope=user/*.cu`: the user is allowed to write (create and update) any type of resource - `scope=[array of individual scopes]` - `patient=123`: the user is allowed access to resources in the Patient/123 [compartment](http://hl7.org/fhir/R4B/compartmentdefinition.html). Wildcard scopes such as `user/*.rs` do not include the [project admin resource types](/docs/access/admin), such as `Project`, `ProjectMembership`, `User` and `AccessPolicy`. A project admin's token reaches those types only through scopes that name them, such as `user/ProjectMembership.rs`. Super admin logins cannot use `patient/`, `user/` or `system/` scopes or a SMART App Launch. ### Authorization UI During authorization, the user has an option to select/deselect scopes and will see a scope selection page like the below. ![Smart scopes example](/img/auth/smart-scopes.png) By default, this page is shown whenever the authorization request asks for any scope other than `openid`. You can override that per client application with `ClientApplication.signInForm.showScopeSelection`: | Value | Behavior | | ------- | ----------------------------------------------------------------------------------------------- | | `true` | Always show the scope selection page. | | `false` | Never show it. The client is granted the full scope that it requested. | | _unset_ | Show it if the authorization request asks for any scope other than `openid` (default behavior). | For example, to suppress the page for a provider-facing app whose users sign in many times a day: ```json { "resourceType": "ClientApplication", "name": "Example EHR App", "signInForm": { "showScopeSelection": false } } ``` :::warning Setting `showScopeSelection` to `false` removes the user's ability to grant the client less access than it requested. Granular scope selection is required for patient-facing apps under [ONC 170.315(g)(10)](https://www.healthit.gov/test-method/standardized-api-patient-and-population-services), so only disable it for provider-facing or trusted first-party clients. ::: --- ## Building a Tenant Selector import ExampleCode from '!!raw-loader!../../../examples/src/access/tenant-selector.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; This document is a follow-up to [Multi-Tenant Access Control](/docs/access/multi-tenant-access-policy). In an MSO Access Control model, a single practitioner often has access to multiple tenants (for example, multiple `Organization` clinics) within the same Medplum `Project`. After login, most applications need an answer to: - Which tenant context is the user working in right now? - When should the UI show a consolidated cross-tenant view vs. a tenant-scoped view? This guide describes three common approaches and how to implement a tenant selector when you need one. ### At a Glance Use the decision tree to pick the option that fits your product, then jump to that section for implementation details. ```mermaid %%{init: {'theme': 'neutral', 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 40}}}%% flowchart LR Q1{"Hard PHI isolation?"} -->|Yes| O1[Option 1: Strict Isolation] Q1 -->|No| Q2{"Tenant-scoped views?"} Q2 -->|Yes| O2[Option 2: Tenant Selector] Q2 -->|No| O3[Option 3: Unified View] style O1 fill:#fce4ec style O2 fill:#e8f5e9 style O3 fill:#e1f5ff ``` | | Option 1: Strict Isolation | Option 2: Single Account + Selector | Option 3: Unified View | | :--- | :--- | :--- | :--- | | API-level tenant isolation | Strongest (one tenant per token) | Allowed tenants enforced; UI scopes within them | None beyond the project | | Consolidated cross-tenant views | Difficult | Supported (when UI omits the filter) | Default | | Tenant-scoped PHI screens | Yes (always) | Yes (when UI applies the filter) | No | | Login / membership model | One membership per tenant per user | One membership covering multiple tenants | One membership covering all tenants | | Tenant switching | Re-auth or saved-login swap | UI control (no re-auth) | N/A | | Implementation complexity | Highest | Medium | Lowest | ### Key Concept: Allowed Tenants vs Active Tenant - Allowed tenants: The set of tenant references the user is permitted to access (enforced by `ProjectMembership` + `AccessPolicy`). - Active tenant: A single tenant reference that your application uses as the default filter for PHI (a UI/application concept). ```mermaid %%{init: {'theme': 'neutral' }}%% flowchart TB U([Practitioner]) --> PM[ProjectMembership] PM --> OA[Org A] PM --> OB["Org B
★ Active Tenant"] PM --> OC[Org C] style PM fill:#e1f5ff style OB fill:#e8f5e9,stroke:#2e7d32,stroke-width:3px ``` _`ProjectMembership` defines the allowed tenants (Org A, B, C). The application picks one as the active tenant (Org B above) and uses it as the default filter on PHI queries._ When you build a "tenant selector", you're choosing and persisting the active tenant. ### Option 1: Strict Isolation (Separate Accounts / Separate Projects) Goal: Hard isolation of PHI at the API level. At any given time, the "active tenant" is the only "allowed tenant" for the user. #### What This Looks Like - The user has separate logins per tenant. - Each login/profile has a `ProjectMembership` that only includes access to one tenant. ```mermaid %%{init: {'theme': 'neutral' }}%% flowchart TB U([Practitioner]) L1["Login 1
ProjectMembership: Org A only"] L2["Login 2
ProjectMembership: Org B only"] OA[Org A patients] OB[Org B patients] U --> L1 U --> L2 L1 --> OA L2 --> OB style L1 fill:#e8f5e9 style L2 fill:#e1f5ff ``` _Each login is bound to a single tenant. Switching tenants requires switching the active login._ :::note In Medplum, a user/profile has a single `ProjectMembership` per `Project` (it is resolved by `project` + `profile`). If you want strict separation *within the same project*, you typically use separate user accounts (separate profiles) rather than "two memberships for one profile". ::: #### Switching the Active `ProjectMembership` In Medplum, the "active membership" is not a client-side flag you can flip on an existing token. The OAuth login flow selects a membership, and the issued access token is bound to that membership. Practically, "switching memberships" means one of: - Switching to another existing login (you already authenticated previously and have a saved token pair). - Starting a new login and selecting a different membership during the login handshake. ##### How Membership Selection Works in the Login Flow - If a user only has one membership, Medplum selects it automatically. - If a user has multiple memberships, the client displays a "choose profile" screen and posts the selected membership ID to `POST /auth/profile`. - After that, the client exchanges the login code for tokens; the token response includes the `project` and `profile` for the selected membership. ##### How Switching Works in the Medplum App / SDK The Medplum App supports "Add another account" and project switching by storing multiple login states locally and switching the active one. In the TypeScript SDK this is done with: - `medplum.getLogins()` to list saved logins - `medplum.setActiveLogin(login)` to switch the active login (and therefore the active membership) ##### How to Configure a User with Multiple Memberships You need to invite the user once per membership you want in the project (for example one membership per tenant in a strict-isolation MSO model). See the [Invite User endpoint](/docs/api/project-admin/invite). Parameterized access policy: put the policy and its substitution values on `membership.access`. Each entry is one [`ProjectMembership.access`](/docs/api/fhir/medplum/projectmembership) rule: a `policy` reference plus an optional `parameter` array. The `name` of each parameter must match your policy placeholders (for example `%organization` uses `"name": "organization"`). A single-tenant membership has one access entry and one parameter object. `forceNewMembership`: set `true` when the user is already a member of this project but you need an additional `ProjectMembership` row (same email address). The first invite for that email in the project does not need it; the second and later ones do, or the server returns a conflict. Example: invite a practitioner with one organization-scoped policy and a single `organization` parameter (replace ids with your `AccessPolicy` and `Organization`). ##### Creating the First `ProjectMembership` {ExampleCode} ##### Creating an Additional `ProjectMembership` {ExampleCode} #### Selector UX Often resembles the Medplum App profile selector: users switch "logins" to switch tenant context. ### Option 2: Single Account, Multi-Tenant Access + UI-Level Tenant Context Goal: One login (`ProjectMembership`) can access one or multiple defined tenants, but the FE application chooses an "active tenant". #### What This Looks Like - One `ProjectMembership` grants access to multiple tenant references (e.g., multiple `Organization` values). - API level access is still enforced to "allowed tenants" only. - Your UI applies a tenant-scoping to the "active tenant" by the search parameter `_compartment`, but can intentionally show consolidated views when needed. ```mermaid %%{init: {'theme': 'neutral' }}%% flowchart TB U([Practitioner]) --> PM["Single ProjectMembership
(Org A + Org B allowed)"] PM --> OA[Org A patients] PM --> OB["Org B patients
★ Active Tenant"] Sel[/"UI Selector
(stored in UserConfiguration)"/] --> OB style PM fill:#e1f5ff style OB fill:#e8f5e9,stroke:#2e7d32,stroke-width:3px style Sel fill:#fff4e1 ``` _One membership grants access to multiple tenants. The UI selector designates one as the active tenant; the user can switch it without re-authenticating._ #### How to Grant Access to Multiple Tenants If your `AccessPolicy` uses `%organization`: ```json { "resourceType": "ProjectMembership", "access": [ { "policy": { "reference": "AccessPolicy/mso-policy" }, "parameter": [ { "name": "organization", "valueReference": { "reference": "Organization/clinic-a" } }, { "name": "organization", "valueReference": { "reference": "Organization/clinic-b" } } ] } ] } ``` With that membership, an access policy criteria like `Patient?_compartment=%organization` can allow access to patients across both tenants. #### Building the Selector At login (or on first use), your app decides an "active tenant" and persists it. Common patterns: - URL: `?tenant=Organization/clinic-a` (easy to deep-link, but must be validated server-side against allowed tenants). - Local storage/session: fast, but not shared across devices. - FHIR-backed preference: store the active tenant in `UserConfiguration` so it follows the user. ##### Persisting the Active Tenant in `UserConfiguration` `UserConfiguration` supports per-user options. One common approach is to store a reference-valued option: ```json { "resourceType": "UserConfiguration", "option": [ { "id": "activeTenant", "valueReference": { "reference": "Organization/clinic-a" } } ] } ``` Then reference that `UserConfiguration` from the user's `ProjectMembership` (see [User Configuration](/docs/access/user-configuration)). ##### Enforcing the Active Tenant in Queries Once you have an active tenant reference (e.g., `Organization/clinic-a`), apply it as a default filter for tenant-scoped screen queries. Examples (MSO model using `Organization` as tenant): - Tenant-scoped PHI view: `Patient?_compartment=Organization/clinic-a` - Consolidated schedule view (cross-tenant): omit tenant filter (or intentionally include multiple tenants) ### Option 3: Unified View (No Tenant Context) Goal: Treat the project as the only boundary; the user always sees everything they're allowed to see. ```mermaid %%{init: {'theme': 'neutral' }}%% flowchart TB U([Practitioner]) --> PM["Single ProjectMembership"] PM --> All["Patients across
Org A + Org B + Org C
(no tenant filter)"] style PM fill:#e1f5ff style All fill:#e8f5e9 ``` _The user sees a single consolidated view of all allowed tenants. There is no active-tenant concept and no UI selector._ #### When to Choose It - Your product model is truly unified (e.g., one clinical ops team across all clinics). - Your contracts and workflows do not require tenant-scoped PHI screens. ### See Also - [Multi-Tenant Access Control](/docs/access/multi-tenant-access-policy) - [Access Policies](/docs/access/access-policies) - [User Configuration](/docs/access/user-configuration) --- ## User Configuration The [UserConfiguration](/docs/api/fhir/medplum/userconfiguration) resource is used to configure user-specific settings or settings that you want a group of users to share. To do this, you need to create a UserConfiguration resource and reference it in each of the Users's [ProjectMembership](/docs/api/fhir/medplum/projectmembership) resources that you want to apply the settings to. For example: ```ts { "resourceType": "ProjectMembership", "userConfiguration": { "reference": "UserConfiguration/", "display": "User Configuration Name" }, //... } ``` ### Custom Menu on the Medplum App Sidebar The Medplum App has a sidebar that displays a list of resources. You can add a custom menu to the sidebar by setting the `UserConfiguration.menu` field. Notice that the target is a FHIR path query that can include filters, sorting, paging, etc. ```ts { "resourceType": "UserConfiguration", "menu": [ { "title": "My Custom Menu", "link": [ { "name": "Patient", "target": "/Patient" }, { "name": "Practitioner", "target": "/Practitioner" }, { "name": "Observations - Heart Rate", "target": "/Observation?code=http://loinc.org|8867-4" } ] } ], //... } ``` ![User Configuration Menu](./custom-menu.png) ### User-specific FHIR Quota Rate Limits The `UserConfiguration.fhirQuota` field is used to configure a custom FHIR quota for a Bot, User, or ClientApplication. See [FHIR Quota Rate Limits](/docs/rate-limits#fhir-interaction-load-rate-limit) for more information. For example: ```ts { "resourceType": "UserConfiguration", "option": [ { "id": "fhirQuota", "valueInteger": 60000 } ], //... } ``` --- ## Modeling your Provider Directory Provider Directories are critical databases housing essential details about healthcare providers, from individual practitioners to entire organizations. Accurate and standardized modeling of these directories, ensures a better patient experience via improved coordination coordination of care and operational efficiency. This section provide a series of guides on how to properly model your organization's provider directory. It focuses on a few key challenges: 1. **[How to model the organizational structure of your provider group:](./provider-directory/provider-organizations)** How to represent the clinical divisions and care teams in your clinical organization, including how to represent providers who participate in multiple teams. 1. [**How to model provider participation insurance networks:**](./provider-directory/provider-networks) How to model insurance plans and the in-network/out-of-network status of providers. 1. [**How to model provider qualifications, certifications, and licenses:**](./provider-directory/provider-credentials) How to represent state licensure and specialty certifications for each provider to ensure proper staffing across geography and clinical. These guides are based on, and compatible with, the [Da Vinci PDEX Payer Network guide](https://build.fhir.org/ig/HL7/davinci-pdex-plan-net/). [The Da Vinci Project](http://www.hl7.org/about/davinci/index.cfm) is a private sector initiative comprised of [industry leaders](https://confluence.hl7.org/display/DVP/Da+Vinci+Project+Members), including Blue Cross Blue Shield, Cigna, and Optum, etc. working together to accelerate the adoption of FHIR as the standard to support and integrate value-based care (VBC) data exchange. ```mermaid flowchart TD A[CareTeam] subgraph clinicians [Clinicians] B[
Practitioner
Dr. Alice Smith
] C[
Practitioner
Bob Ross, NP
] end A-->|member| B A-->|member| C A-->|subject| D[
Patient
Homer Simpson
] ``` ### Key Resources | **Resource** | **Description** | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | [`CareTeam`](/docs/api/fhir/resources/careteam) | A definition of a group of providers assembled for a specific [`Patient`](/docs/api/fhir/resources/patient). | | [`Practitioner`](/docs/api/fhir/resources/practitioner) | A resource that defines the profile of each provider (i.e., physician, nurse, administrator, etc.). | ### Key Code Systems | **Code System** | **Description** | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [SNOMED](https://www.snomed.org/) | Used to annotate the roles of [`Practitioner`](/docs/api/fhir/resources/practitioner) resources. Below is a selection of common [SNOMED](https://www.snomed.org/) role codes. | | Role | SNOMED Code | | ----------------------- | ----------- | | Primary Care Physician | 446050000 | | Registered Nurse | 224535009 | | Social Worker | 106328005 | | Physical Therapist | 36682004 | | Occupational Therapist | 80546007 | | Recreational Therapist | 308191009 | | Dietitian | 159033005 | | Center Manager | 159441000 | | Home Care Coordinator | 768730001 | | Personal Care Attendant | 768836001 | | Driver | 236320001 | --- ## Modeling Provider Qualifications import ExampleCode from '!!raw-loader!@site/../examples/src/fhir-datastore/provider-credentials.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; ### Introduction For providers that operate across multiple regions and specialties, properly representing provider credentials is critical for regulatory compliance, insurance billing, and referral management. In the U.S., virtual providers must ensure that physicians are licensed in the same state as their patients. Another important operational consideration is making sure that care coordinators, nurses, and doctors are practicing at the top of their license. ### Key Elements The key FHIR element for storing all licenses and degrees is the `Practitioner.qualifications` element. This is an array of all degrees, licenses, and certifications achieved achieved by the provider. The two most important sub-elements are: | Element | Description | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `Practitioner.qualifications[i].code` | This [`CodeableConcept`](/docs/fhir-basics#standardizing-data-codeable-concepts) is used to identify the type of certification | | `Practitioner.qualifications[i].issuer` | The organization responsible for issuing the certification | The next sections discuss how to populate these elements for state medical licenses and specialty board certifications. :::tip[Looking up provider data] Rather than entering NPI, taxonomy, and license data by hand, you can pull much of it from the public [NPPES NPI Registry](https://npiregistry.cms.hhs.gov/api-page) maintained by CMS. See the [NPPES Lookup Bot](https://github.com/medplum/medplum/tree/main/examples/medplum-demo-bots/src/nppes-lookup.ts) for a thin wrapper around the registry API that you can call from another bot to populate `Practitioner.identifier` (NPI), `Practitioner.qualification`, and `PractitionerRole.specialty` (NUCC taxonomy codes). ::: ### Medical Licensure In the U.S., medical licenses are issued by each state's medical board, and are only valid for patients in that state. Use the HL7 `degreeLicenseCertificate` code system (http://terminology.hl7.org/CodeSystem/v2-0360) to model the license level achieved by the provider (MD, DO, RN, NP, etc.). The `issuer` is typically the state (e.g., "State of New York"). Although this element is typically a reference to an `Organization`, in for the sake of simplicity we recommend simply use the `display` element, rather than creating an `Organization` resource for each state. To augment the `issuer`, the Davinci PDEX implementation guide [defines an extension](https://build.fhir.org/ig/HL7/davinci-pdex-plan-net/StructureDefinition-practitioner-qualification.html) for `Practitioner.qualification`, named `practitioner-qualification`. This extension contains an element, `whereValid` , which allows you to represent medical license jurisdictions using USPS postal codes for convenience.
Example {ExampleCode}
### Medical Specialty A provider's specialty certifications can also be represented Unlike licensure, medical specialties are determined by professional boards of physicians, not governments. Here are the key points to consider: - **Specialty Code**: The PDEX implementation guide requires selecting a provider's specialty code from the [NUCC provider taxonomy](https://taxonomy.nucc.org/) (system: `http://nucc.org/provider-taxonomy`). - **Issuer: **The issuer for specialist certifications are typically professional certification boards (e.g. American Board of Internal Medicine or American College of Obstetricians and Gynecologists )
Example {ExampleCode}
:::tip[`Practitioner.qualification` vs. `PractitionerRole.specialty`] Both the `Practitioner.qualification` and `PractitionerRole.specialty` elements can be used to represent a provider's specialization, but there are important differences between how they are used. - `Practitioner.qualification` indicates the specific qualifications that a provider has obtained, and are associated with the provider's _person_. This is useful for illustrating a provider's overall skill set, education, and training, regardless of their current role or where they're practicing. - `PractitionerRole.specialty` indicates the particular specialty a provider is currently practicing in a specific context or organization. This is more flexible as a provider can have different roles in different organizations or at different times. For example, a practitioner who is qualified as both a cardiologist and an internist might serve as a cardiologist at one hospital and an internist at another. See our guide on [Modeling Practitioner Organizations](./provider-organizations) for more information about modeling providers who work across multiple organizations. ```mermaid graph LR A[Practitioner\nJohn Smith\n\nQualifications:\n
  • Medical Doctor
  • Internal Medicine
  • Cardiology
] E[PractitionerRole
Specialty: Internal Medicine] F[PractitionerRole
Specialty: Cardiology] G[PractitionerRole
Specialty: Internal Medicine, Cardiology] A ---> E A ---> F A ---> G H[Organization 1] I[Organization 2] J[Organization 3] E -- organization --> H F -- organization --> I G -- organization --> J ``` ::: ### Conclusion The `Practitioner.qualification` element is the place to store all Practitioner licenses and credentials. The key is to use standard code systems to support data exchange with other systems. In this guide we've summarized the main points of the [Davinci PDEX Payer Network](https://build.fhir.org/ig/HL7/davinci-pdex-plan-net/index.html) IG to serve a starting point for your implementation. --- ## Modeling Provider Networks ### Introduction Understanding how to effectively track and manage physician networks is crucial for digital health companies aiming to offer affordable and convenient care. This guide will provide an in-depth explanation on: - Modeling an insurance provider network offered by a payer. - Associating an individual or organizational provider with an insurance network. - Modeling an insurance product or plan offered by a payer. ### Defining the Network A critical first step is to define the insurance network. A network is a group of doctors, hospitals and care providers that the health insurance company contracts with to provide medical services. In FHIR, an insurance network is modeled using the `Organization` resource. This resource represents the many-to-many relationship between insurance plans and provider networks. The Da Vinci PDEX Plan Net (PDEX) specifies a [unique profile](http://hl7.org/fhir/us/davinci-pdex-plan-net/StructureDefinition-plannet-Network.html) of `Organization` to represent a provider network. In this profile, the `partOf` attribute is mandatory, necessitating the creation of an `Organization` resource for each payer. ```mermaid flowchart BT 324537["Network (Organization)\nAcme Insurance Standard Network"] --partOf --> 344639["Organization\nAcme Insurance"] 431222["Network (Organization)\nAcme Insurance Premium Network"] -- partOf --> 344639 ``` ### Connecting Providers to Networks To link individual `Practitioners` to the network, the `PractitionerRole` resource is used. For every network affiliation, there should be a corresponding `PractitionerRole`. The PDEX extends the `PractitionerRole` resource with a `network` attribute to reference the network. For instance, a single practitioner operating in two different organizations can each point to the same Acme Bronze network. ```mermaid flowchart LR A["Practitioner\nDr. Alice Smith"] ---> E["PractitionerRole"] A ---> F["PractitionerRole"] E -->|"organization"| H["Organization\#110;Hospital 1"] F -->|"organization"| I["Organization\#110;Hospital 2"] E -->|"network"| G["Network (Organization) \#110;Acme Insurance Standard Network"] F -->|"network"| G ``` Organizational providers (e.g. "Cancer Specialists of Springfield"), are modeled as `Organization` resources. The `OrganizationAffiliation` resource is used to link the provider to the network through the `OrganizationAffiliation.network` attribute. ### Connecting Insurance Plans to Networks The `InsurancePlan` resource represents a health insurance offering by a payer. It consists of a list of covered benefits (`InsurancePlan.coverage`), costs associated with those benefits (`InsurancePlan.plan`), and additional details about the offering. The `InsurancePlan.plan` attribute describes the cost-sharing agreement between the payer and the patient. There can be multiple `InsurancePlan.plan` entries. This is because the same insurance product may have different costs for beneficiaries, depending on whether the provider is in-network or out-of-network. A plan can be associated with multiple networks, and a network can be associated with many plans. You can specify the `InsurancePlan.network` attribute and/or the `InsurancePlan.network` attribute that applies to all cost-sharing plans. If the `InsurancePlan.network` attribute is not provided, each cost-sharing plan must provide at least one network in the `InsurancePlan.plan.network` attribute. ```mermaid flowchart BT 467375["Insurance Plan\nAcme QHP Gold"] -->|"network"| 569274["Network\nAcme Insurance Standard Network"] 467375 -->|"network"| 598156["Network\nAcme Insurance Premium Network"] 775427["Insurance Plan\nAcme QHP Bronze"] -->|"network"| 598156 ``` ### References As a private sector collaboration [Da Vinci PDEX Payer Network guide](https://build.fhir.org/ig/HL7/davinci-pdex-plan-net/) is being implemented by Payers. See the following reference APIs for examples 1. [Humana Insurance Plan API](https://developers.humana.com/apis/insuranceplan-api/doc) 2. [Alliance Health Developer Resources](https://thealliance.health/developer-resources/provider-directory/) For information about how to represent provider directories, see our guides on [Provider Organizations](./provider-organizations) and [Provider Credentials](./provider-credentials). --- ## Modeling Provider Organizations ### Introduction Large digital healthcare organizations frequently employ teams of physicians, organized by specialty, brand, and/or product offering. In this guide, we'll discuss how to model the structure of a large provider organization with multiple divisions and service lines. We'll cover - How to model divisions and sub-divisions inside a provider group - How to model provider care teams - How to assign practitioners to multiple divisions and teams ### Modeling the Organizational Hierarchy To model groups, it is recommended to use the `Organization` resource. `Organizations` model how people are grouped, and can be organized in a hierarchical structure by utilizing the `partOf` reference field. Let's look at the example organization called "Acme Provider Group" with four clinical divisions, "Mental Health", "Dermatology", "Sexual and Reproductive Health", and "Diet and Nutrition". You would create an `Organization` for the top level Acme Provider Group. Each of the four divisions would then be represented as separate `Organization` entities and connected to the parent organization using the `partOf` reference field. You can further divide each of these divisions, such as "Men's Health" and "Women's Health" divisions within Sexual and Reproductive Health. ```mermaid graph BT A["Acme Provider Group"] B["Mental Health"] C["Dermatology"] D["Sexual and Reproductive Health"] E["Diet and Nutrition"] F["Men's Health"] G["Women's Health"] F -->|partOf| D G -->|partOf| D B -->|partOf| A C -->|partOf| A D -->|partOf| A E -->|partOf| A ``` **Note:** This hierarchical structure works well for digital health practices with organizational monoliths. To model more complex many-to-many relationships, refer to the `OrganizationAffiliation` resource. ### Modeling Care Teams At the bottom of the hierarchy, you may have teams of providers that work closely together to provide care. We advise using the the `CareTeam` resource to model these provider teams. The `CareTeam` resource enables you to easily specify members of a team through the `participant` field and assign roles within the team. The US Core guidelines recommend that `CareTeam.participant.role` be selected from the [Care Team Member Function](https://vsac.nlm.nih.gov/valueset/2.16.840.1.113762.1.4.1099.30/expansion) ValueSet, which is a subset of the SNOMED CT code system. ```mermaid graph TD A["CareTeam"] B["Practitioner: Primary Care Physician"] C["Practitioner: Registered Nurse"] D["Practitioner: Care Coordinator"] A -->|participant.role: Primary Care Physician| B A -->|participant.role: Registered Nurse| C A -->|participant.role: Care Coordinator| D ``` :::tip[`Organization` vs. `CareTeam`] `Organization` and `CareTeam` are both FHIR resources that are used to represent different aspects of healthcare providers and teams. While you could use an `Organization` to achieve similar goals, `CareTeam` is more fluid and easier to update. Some of the other differences between the `Organization` and `CareTeam` resources include: **Purpose** - `Organization`: Represents an organization such as a hospital, clinic, or insurance company. It can be any legal entity involved in the provision or management of healthcare services. - `CareTeam`: Represents a group of healthcare professionals and support staff that work together to provide care for a specific patient. It focuses on the collaborative aspect of care delivery. **Structure** - `Organization`: Primarily captures information about the organization itself, such as name, address, contact details, and type (e.g., hospital, laboratory, etc.). - `CareTeam`: Captures information about the members of the care team, their roles, and responsibilities, as well as the specific patient they are caring for and the period of care. **Relationship** - `Organization`: Can be related to other FHIR resources, such as `Practitioner`, `Location`, or `Patient`, to indicate the affiliation of healthcare professionals or the location where care is provided. - `CareTeam`: Can be related to resources like `Encounter`, `EpisodeOfCare`, or `Condition`, to provide context for the care team's involvement in a specific healthcare scenario or the management of a specific condition. **Scope** - `Organization`: Typically represents a broader scope of entities involved in healthcare, including not only care providers but also payers, regulators, and suppliers. - `CareTeam`: Focuses on the narrower scope of collaboration among healthcare professionals and support staff, specifically in the context of patient care. **Usage** - `Organization`: Useful for managing healthcare provider networks, credentialing, and facility management. - `CareTeam`: Useful for care coordination, case management, and tracking patient-specific care activities. ::: ### Modeling Service Lines The `Organization` resource is designed to model how a practice is _structured_, while the `HealthcareService` resource focuses on their _function_. In the context of digital health, the `HealthcareService` resource can be thought of as a "service line" or even a "product offering." Not all digital health companies will require the use of the `HealthcareService` resource. Many practices offer a single service line per division. However, some practices have providers working on distinct services within the same organization, with certain providers working on multiple services simultaneously. Adding a `HealthcareService` resource can help keep track of which providers are assigned to each service (see diagram in next section) For instance, in the "Acme Provider Group" example, the "Women's Health" group might offer both STD and fertility services, with some providers working on one or both services. To assign a specific service to a given organization, you can use the `HealthcareService.providedBy` field. This allows you to accurately represent the various services offered by providers within a large physician group or healthcare organization. ### Assigning Providers to Organizations ```mermaid graph LR A["👩🏽‍⚕️ Practitioner: Dr. Alice Smith"] --> B["PractitionerRole1\nspecialty: Ob/Gyn"] A --> C["PractitionerRole2\nspecialty: Internal Medicine"] E["🏥 Sexual Reproductive Health"] F["🏥 Dermatology"] G["🩺 HealthcareService: Fertility"] H["🩺 HealthcareService: STD"] H --providedBy--> E G --providedBy--> E B -- organization --> E C -- organization --> F B -- healthcareService --> G B -- healthcareService --> H ``` The `Practitioner` resource in FHIR is used to describe the _person_ who is the healthcare provider. Each provider should have only one `Practitioner` resource, which contains their name, demographic information, contact information, and qualifications (refer to our guide on Provider Credentials for more information). This resource is also used as their profile during [authentication and authorization](/docs/auth) The `PractitionerRole` is used to assign practitioners to `Organizations`, `CareTeams`, and `HealthcareServices`. Each practitioner should have one `PractitionerRole` per organization. If a practitioner participates in multiple `Organizations`, multiple `PractitionerRoles` should be created. Within each `PractitionerRole`, it is possible to assign a provider to be to multiple `HealthcareServices` to indicate the full scope of work the provider performs. There are two additional fields that describe _how_ the provider participates in the organization: - `PractitionerRole.code` describes roles which this practitioner may perform as part of the organization, and uses the same value set as `CareTeam.participant.role` ([Care Team Member Function](https://vsac.nlm.nih.gov/valueset/2.16.840.1.113762.1.4.1099.30/expansion)) - `PractitionerRole.specialty` can be used to indicate which specialty a provider is performing _at a given organization._ For instance, Provider A may act as a Cardiologist at Organization 1, but as an Internal Medicine Doctor at Organization 2. The recommended terminology for this field is the [NUCC Health Care Provider Taxonomy](https://www.nucc.org/index.php/code-sets-mainmenu-41/provider-taxonomy-mainmenu-40) When adding a practitioner to a `CareTeam`, it is important to add the `PractitionerRole` resource, rather than the `Practitioner` resource itself. This approach makes it easier to keep track of in-network and out-of-network providers for billing purposes (see our guide on Modeling Insurance Networks more information on this topic). ```mermaid graph TD A[SRH Care Team 1] B[SRH Care Team 2] C[PractitionerRole:\nJane Smith, MD] D[PractitionerRole:\nBob Cheng, RN] E[PractitionerRole:\nJulie Cooper, MD] A --participant--> C A --participant--> D B --participant--> D B --participant--> E ``` ### Conclusion The `Organization`, `CareTeam`, and `HealthcareService` resources are the primary building blocks for modeling your practice, and the `Practitioner` and `PractitionerRole` resources allow you to flexibly model your provider group. While the model described works well for an in-house group or PC, more complicated organizations and coordination with 3rd parties might require more complicated models. Consult the [Da Vinci Plan Network Implementation Guide](https://build.fhir.org/ig/HL7/davinci-pdex-plan-net/index.html) or contact the [Medplum team](https://discord.gg/medplum) for more information! --- ## Agent Access Policy This guide covers the minimal access policy configuration required for the Medplum Agent, including how to use parameterized policies for multi-organization deployments. ### Minimal Access Policy The following `AccessPolicy` provides the bare minimum permissions needed to start the agent and invoke a simple Bot that doesn't create any resources: ```json { "resourceType": "AccessPolicy", "name": "Minimal Agent Access Policy", "resource": [ { "resourceType": "Endpoint", "readonly": true }, { "resourceType": "Agent", "readonly": true }, { "resourceType": "Bot", "readonly": true }, { "resourceType": "CodeSystem", "readonly": true }, { "resourceType": "ValueSet", "readonly": true } ] } ``` This policy grants read-only access to: - **Endpoint**: Required to read the channel endpoint configurations - **Agent**: Required to read the agent configuration itself - **Bot**: Required to read and invoke bots defined in the agent channels - **CodeSystem** and **ValueSet**: Required for terminology operations that bots may use ### Parameterized Access Policy For deployments with multiple organizations, you can use a [parameterized access policy](/docs/access/access-policies#parameterized-policies) to share a single policy across all `ClientApplication` resources. This uses the `%org` variable to scope resources to the appropriate organization: ```json { "resourceType": "AccessPolicy", "name": "Minimal Agent Access Policy", "resource": [ { "resourceType": "Endpoint", "readonly": true, "criteria": "Endpoint?_compartment=%org" }, { "resourceType": "Agent", "readonly": true, "criteria": "Agent?_compartment=%org" }, { "resourceType": "Bot", "readonly": true, "criteria": "Bot?_compartment=%org" }, { "resourceType": "CodeSystem", "readonly": true }, { "resourceType": "ValueSet", "readonly": true } ] } ``` ### Adding Bot Resource Permissions Bot invocations inherit permissions from the invoking `ClientApplication`. You must add entries to the access policy for any resource types that your bots need to read or write. For example, if your bot creates `Patient` resources as part of its workflow, add read/write access: ```json { "resourceType": "AccessPolicy", "name": "Minimal Agent Access Policy", "resource": [ { "resourceType": "Endpoint", "readonly": true, "criteria": "Endpoint?_compartment=%org" }, { "resourceType": "Agent", "readonly": true, "criteria": "Agent?_compartment=%org" }, { "resourceType": "Bot", "readonly": true, "criteria": "Bot?_compartment=%org" }, { "resourceType": "CodeSystem", "readonly": true }, { "resourceType": "ValueSet", "readonly": true }, { "resourceType": "Patient", "criteria": "Patient?_compartment=%org" } ] } ``` ### Using Compartments for Resource Tagging You can add a top-level `compartment` to the access policy to automatically tag all resources created by the bot with the appropriate organization. This ensures that resources are forced into the correct compartment: ```json { "resourceType": "AccessPolicy", "name": "Minimal Agent Access Policy", "compartment": { "reference": "%org" }, "resource": [ { "resourceType": "Endpoint", "readonly": true, "criteria": "Endpoint?_compartment=%org" }, { "resourceType": "Agent", "readonly": true, "criteria": "Agent?_compartment=%org" }, { "resourceType": "Bot", "readonly": true, "criteria": "Bot?_compartment=%org" }, { "resourceType": "CodeSystem", "readonly": true }, { "resourceType": "ValueSet", "readonly": true }, { "resourceType": "Patient", "criteria": "Patient?_compartment=%org" } ] } ``` With this configuration, any `Patient` resources created by the bot will automatically have their `meta.account` set to the organization specified in the policy parameter. ### Configuring the ProjectMembership To use a parameterized access policy, you must configure the `ProjectMembership` for the `ClientApplication` with the appropriate parameter values. The `org` parameter should reference the `Organization` resource that the agent belongs to: ```json { "resourceType": "ProjectMembership", "access": [ { "policy": { "reference": "AccessPolicy/your-access-policy-id", "display": "Minimal Agent Access Policy" }, "parameter": [ { "name": "org", "valueReference": { "reference": "Organization/your-organization-id", "display": "My Organization" } } ] } ] } ``` This configuration allows you to: 1. Use the same `AccessPolicy` resource for all agent `ClientApplication` resources 2. Scope each agent's access to resources within its specific organization 3. Automatically tag resources created by bots with the correct organization compartment ### See Also - [Access Policies](/docs/access/access-policies) - Full documentation on access policies - [Parameterized Policies](/docs/access/access-policies#parameterized-policies) - Details on using variables in access policies - [Intro to Medplum Agent](/docs/agent) - Agent setup and configuration --- ## Acknowledgement Modes HL7 Acknowledgement Modes define the handshaking protocol between systems exchanging messages over protocols like MLLP. The **Medplum Agent** supports both **Original** and **Enhanced** modes, allowing you to configure connections for optimal throughput and reliability. Support for Enhanced Acknowledgement Mode was added in **Medplum Server version 3.1.9** and **Medplum Agent version 4.1.8**. ### Overview of HL7 Acknowledgements (ACKs) HL7 (Health Level Seven) is the standard used for exchanging clinical and administrative data. When a system (the **Sender**) transmits an HL7 message to another system (the **Receiver**), the Receiver is required to send back an **Acknowledgement (ACK)** message to confirm the transaction status. #### The Purpose of Acknowledgements Acknowledgements are essential for reliable messaging. They serve to: 1. **Confirm Delivery:** Verify that the message was received by the destination system. 2. **Communicate Errors:** Notify the Sender immediately if a structural or application-level problem occurs. 3. **Ensure Data Integrity:** Provide explicit confirmation (success or failure) to the Sender, allowing them to decide whether to retry the transmission and preventing data loss or duplication. The acknowledgement status is communicated within the **MSA** (Message Acknowledgment) segment. | Code (Table 0008\) | Meaning | Description | | :----------------- | :----------------- | :----------------------------------------------------------------------------------------------------------- | | **AA** | Application Accept | Receiving application successfully processed the message. | | **AE** | Application Error | Receiving application found an error in processing the message. | | **AR** | Application Reject | Receiving application failed to process message for a non-content reason (e.g., security or sequence issue). | ### Original Acknowledgement Mode Original Mode is the traditional, single-step confirmation process. It is the default protocol for HL7 communication and is often referred to as a "Receive" ACK. #### How Original Mode Works In Original Mode, the Sender transmits a message and must **block** (wait) on that connection until it receives a single **Application Acknowledgement (AA, AE, or AR)** from the Receiver. 1. **Sender** transmits Message A. 2. **Receiver** takes Message A and runs it through its entire processing pipeline (validation, database commits, complex business logic). 3. **Receiver** sends a single Application ACK (AA/AE/AR). 4. **Sender** receives the ACK and then transmits Message B. #### The Bottleneck This single-step, blocking approach makes the connection inherently **single-threaded** and inefficient for high-volume data streams. The connection is locked while the receiving application performs the slow, heavy work of processing. This often limits message throughput to low rates, typically **3-4 messages per second (msg/sec)**. ### Enhanced Acknowledgement Mode (Fast ACK) **Enhanced Acknowledgement Mode**, sometimes called **Fast ACK**, resolves the performance limitations of Original Mode by separating the delivery confirmation from the application processing result. This mode requires the exchange of **two separate acknowledgements**. #### How Enhanced Mode Works (Two-Step Process) ##### Step 1: Accept Acknowledgement (The Fast Response) Immediately upon receipt, the receiving system (or Medplum Agent) performs quick structural checks and **commits the message to safe, durable storage** (like a queue or database). It then sends a commitment confirmation back to the Sender. This step is designed to be extremely fast, quickly unblocking the connection. | Code (v3 AcknowledgementType) | Meaning | Description | | :---------------------------- | :------------ | :------------------------------------------------------------------------------------------------------------- | | **CA** | Commit Accept | Receiving system/handling service accepts responsibility for the message and has committed it to safe storage. | | **CE** | Commit Error | Receiving system cannot accept the message for a structural/system reason (e.g., sequence error). | | **CR** | Commit Reject | Receiving system rejects message due to incompatibility (e.g., version mismatch). | ##### Step 2: Application Acknowledgement (The Processing Result) Once the Accept ACK (**CA**) is sent, the MLLP connection is free to immediately receive the next message. The actual message processing (the slow business logic) happens **asynchronously** in the background. After the message has been fully processed by the target application, the final **Application Acknowledgement (AA/AE/AR)** is generated and sent back to the Sender. #### Performance By unblocking the connection after the quick **CA** response, the Enhanced Mode achieves significantly higher throughput. Implementations using Enhanced Mode with the Medplum Agent have demonstrated performance improvements, moving from single-digit rates to over **300-400 messages per second** in high-volume production environments. #### MSH Configuration for Enhanced Mode The choice of acknowledgement mode is negotiated using two fields in the MSH segment of the HL7 message: - **MSH-15 (Accept Acknowledgment Type):** Dictates when the Sender expects the _Accept ACK_ (CA/CE/CR). - **MSH-16 (Application Acknowledgment Type):** Dictates when the Sender expects the _Application ACK_ (AA/AE/AR). Common values for these fields include: | Value | Condition | | :----- | :----------------------------------------------------------------------- | | **AL** | **Always:** Send the ACK regardless of success or failure. | | **NE** | **Never:** The Sender does not require this type of ACK. | | **ER** | **Error:** Only send the ACK if an error occurs. | | **SU** | **Success:** Only send the ACK if the message is processed successfully. | #### Compatibility Caution Enhanced Mode is not universally supported across all legacy HL7 systems. It is vital to confirm with the external system vendor (the Sender or the Receiver) that they explicitly support the two-step handshaking process before configuring the Medplum Agent to use Enhanced Acknowledgement Mode. ### AA Mode (Simplified Enhanced Mode) **AA Mode** is a specialized variant of Enhanced Acknowledgement Mode that provides enhanced throughput benefits without requiring the remote system to support or be configured for the full two-step acknowledgement handshake. This mode was introduced in **Medplum Agent version 5.0.11**. #### How AA Mode Works In AA Mode, the Medplum Agent immediately sends an **Application Accept (AA)** acknowledgement upon receipt of a message, rather than the Commit Accept (CA) used in standard Enhanced Mode. Since AA is a standard application-level ACK code that all HL7 systems understand, the remote system does not need any special configuration to work with this mode. 1. **Sender** transmits Message A. 2. **Medplum Agent** immediately sends an **AA** (Application Accept) acknowledgement. 3. **Connection is unblocked** — Sender can immediately transmit Message B. 4. Message A is processed asynchronously in the background. #### When to Use AA Mode AA Mode is ideal when: - **The remote system doesn't support Enhanced Mode:** Many legacy HL7 systems only understand original mode acknowledgements (AA/AE/AR). AA Mode allows you to achieve enhanced throughput while sending ACKs the remote system can process. - **You cannot configure the remote system:** In scenarios where you have no control over the remote system's configuration, AA Mode works without any changes on the peer's side. - **You need fast throughput but don't require processing feedback:** If your workflow doesn't need to communicate application-level success or failure back to the sender, AA Mode provides the performance benefits of enhanced mode with simpler compatibility. #### Drawbacks AA Mode has an important trade-off: - **No asynchronous application-level feedback:** Since the AA is sent immediately (before processing), you lose the ability to send back application-level error responses (AE) or rejections (AR) after processing completes. The remote system will always receive AA, regardless of whether the message was successfully processed by your application. This means: - Processing errors cannot be communicated back to the sender via HL7 ACKs - The sender has no mechanism to know if the message failed during application processing - You must implement alternative error handling mechanisms if processing feedback is required (e.g., logging, monitoring, or out-of-band notifications) #### Configuration To enable AA Mode, add `enhanced=aa` to your Agent channel endpoint URL: ``` mllp://0.0.0.0:2575?enhanced=aa ``` | Parameter Value | Mode | Immediate ACK | Application ACK Forwarded | | :-------------- | :---------------- | :------------ | :------------------------ | | `enhanced=true` | Standard Enhanced | CA | Yes (AA/AE/AR) | | `enhanced=aa` | AA Mode | AA | No | | _(not set)_ | Original Mode | _(none)_ | Yes (AA/AE/AR) | #### Comparison: Standard Enhanced vs AA Mode | Aspect | Standard Enhanced Mode | AA Mode | | :------------------------------ | :---------------------------- | :------------------------------------ | | **Immediate ACK Code** | CA (Commit Accept) | AA (Application Accept) | | **Remote Configuration** | Required (must support CA) | Not required | | **Application ACK Forwarding** | Yes | No | | **Processing Feedback** | Full (AA/AE/AR after processing) | None (always AA) | | **Throughput Improvement** | Yes | Yes | | **Best For** | Full enhanced mode support | Legacy systems, no peer configuration | --- ## Agent CLI Commands The Medplum CLI provides several commands for managing and interacting with agents. This document describes the available commands and their usage. See more about installing and using the Medplum CLI [here](/docs/cli). ### Status Command Get the status of one or more agents. ```bash medplum agent status [agentIds...] [options] ``` #### Arguments - `[agentIds...]`: List of agent IDs to get status for. Either this or `--criteria` must be provided. #### Options - `--criteria `: FHIR search criteria to find agents (mutually exclusive with agentIds) - `--output `: Output format (table or json, defaults to table) #### Example ```bash # Get status for specific agents medplum agent status 123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426614174001 # Get status using search criteria medplum agent status --criteria "Agent?name=Test Agent" # Get status in JSON format medplum agent status --criteria "Agent?name=Test Agent" --output json ``` ### Ping Command Ping a host from a specified agent. ```bash medplum agent ping [options] ``` #### Arguments - ``: The IPv4 address or domain name to ping - ``: ID of the agent to ping from. Either this or `--criteria` must be provided. #### Options - `--count `: Number of pings to issue (default: 1) - `--criteria `: FHIR search criteria to find the agent (mutually exclusive with agentId) #### Example ```bash # Ping using specific agent medplum agent ping example.com 123e4567-e89b-12d3-a456-426614174000 # Ping using search criteria medplum agent ping example.com --criteria "Agent?name=Test Agent" # Ping multiple times medplum agent ping example.com --criteria "Agent?name=Test Agent" --count 5 ``` ### Push Command Push a message to a target device via a specified agent. ```bash medplum agent push [options] ``` #### Arguments - ``: The ID of the device to push the message to - ``: The message to send to the destination device - ``: ID of the agent to send the message from. Either this or `--criteria` must be provided. #### Options - `--content-type `: The content type of the message (default: HL7_V2) - `--no-wait`: Don't wait for a response from the destination device - `--criteria `: FHIR search criteria to find the agent (mutually exclusive with agentId) #### Example ```bash # Push message using specific agent medplum agent push device123 "Hello World" 123e4567-e89b-12d3-a456-426614174000 # Push message using search criteria medplum agent push device123 "Hello World" --criteria "Agent?name=Test Agent" # Push message without waiting for response medplum agent push device123 "Hello World" --criteria "Agent?name=Test Agent" --no-wait ``` ### Reload Config Command Reload the configuration for one or more agents. ```bash medplum agent reload-config [agentIds...] [options] ``` #### Arguments - `[agentIds...]`: List of agent IDs to reload config for. Either this or `--criteria` must be provided. #### Options - `--criteria `: FHIR search criteria to find agents (mutually exclusive with agentIds) - `--output `: Output format (table or json, defaults to table) #### Example ```bash # Reload config for specific agents medplum agent reload-config 123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426614174001 # Reload config using search criteria medplum agent reload-config --criteria "Agent?name=Test Agent" ``` ### Upgrade Command Upgrade the version for one or more agents. ```bash medplum agent upgrade [agentIds...] [options] ``` #### Arguments - `[agentIds...]`: List of agent IDs to upgrade. Either this or `--criteria` must be provided. #### Options - `--criteria `: FHIR search criteria to find agents (mutually exclusive with agentIds) - `--version `: Version to upgrade to (defaults to latest version) - `--output `: Output format (table or json, defaults to table) #### Example ```bash # Upgrade specific agents to latest version medplum agent upgrade 123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426614174001 # Upgrade using search criteria medplum agent upgrade --criteria "Agent?name=Test Agent" # Upgrade to specific version medplum agent upgrade --criteria "Agent?name=Test Agent" --version 1.2.3 ``` ### Stats Command Get runtime statistics for one or more agents. ```bash medplum agent stats [agentIds...] [options] ``` #### Arguments - `[agentIds...]`: List of agent IDs to get stats for. Either this or `--criteria` must be provided. #### Options - `--criteria `: FHIR search criteria to find agents (mutually exclusive with agentIds) - `--output `: Output format (table or json, defaults to table) #### Example ```bash # Get stats for specific agents medplum agent stats 123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426614174001 # Get stats using search criteria medplum agent stats --criteria "Agent?name=Test Agent" # Get stats in JSON format medplum agent stats --criteria "Agent?name=Test Agent" --output json ``` ### Common Options All commands support the following common options: - `--fhir-url `: FHIR server URL - `--client-id `: OAuth client ID - `--client-secret `: OAuth client secret - `--access-token `: Access token for authentication ### Error Handling The CLI provides detailed error messages when operations fail. Common error scenarios include: - Invalid agent IDs or search criteria - Network connectivity issues - Authentication failures - Agent not found - Operation timeouts When using the `--output json` option, errors are returned in a structured format that can be parsed programmatically. --- ## ASTM Channels Clinical analyzers — Bio-Rad, Beckman, Roche, and many others — commonly speak **ASTM E1381** (the link layer) carrying **ASTM E1394** (the record format). The Agent handles these with a [byte stream channel](./features.md), configured entirely through query parameters on the channel's `Endpoint.address`. This page covers the configuration those analyzers need, and the behaviors that most often surprise people integrating one for the first time. :::info Transport: raw TCP only Everything here describes ASTM carried over a **raw TCP stream**. A byte stream channel listens on a TCP port, so an analyzer that presents a serial (RS-232) interface — still the majority — has to reach the Agent through a serial-to-TCP/IP adapter, sometimes sold as a serial device server or terminal server. The adapter passes bytes through untouched, so the ASTM framing and the configuration below are unaffected by it. Connecting to a serial port directly from the Agent is **not yet supported**, but is planned. Until then an adapter is required for serial-only analyzers. ::: ### Recommended configuration ``` tcp://0.0.0.0:9004 ?startChar=%05&endChar=%04 &autoRespond=%05:%06,%03:%06,%17:%06 &stripControlChars=true&keepControlChars=%0D%0A &bodyEncoding=utf-8 &ignoreResponse=true ``` (Shown wrapped for readability; the real value is a single line.) | Parameter | Why an ASTM channel needs it | | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `startChar=%05&endChar=%04` | Frames on `ENQ`/`EOT`, so one **ASTM session** becomes one message. See [Framing](#framing-one-session-per-message). | | `autoRespond=%05:%06,%03:%06,%17:%06` | Answers `ENQ`, `ETX`, and `ETB` with `ACK`, which is the full set of bytes E1381 acknowledges. See [Acknowledgement](#link-level-acknowledgement). | | `stripControlChars=true` | Removes the `ENQ`, `STX`, `ETX`/`ETB` and `EOT` framing, leaving readable records. | | `keepControlChars=%0D%0A` | Exempts `CR` and `LF` from that sweep. ASTM records are `CR`-terminated; without this the whole message is one run-on line. | | `bodyEncoding=utf-8` | Delivers the body to the Bot as text. The default, `hex`, would require the Bot to decode it first. | | `ignoreResponse=true` | Makes the channel one-way, for analyzers that never read a reply. See [One-way channels](#one-way-channels). | ### Framing: one session per message An ASTM session looks like this on the wire: ``` --> ENQ <-- ACK --> STX 1 H|\^&|||... CR ETX C1C2 CR LF <-- ACK --> STX 2 P|1|... CR ETX C1C2 CR LF <-- ACK --> EOT ``` Framing on `ENQ`/`EOT` (`startChar=%05&endChar=%04`) makes the Agent treat everything between them as a single message, so the Bot receives the complete result set — `H` through `L` — in one invocation. :::danger Do not frame on `STX`/`ETX` `startChar=%02&endChar=%03` looks natural, since `STX` and `ETX` delimit each frame. It will deliver **one message per frame**, so a Bot that needs the header, order, and result records together sees them split across several invocations with no way to correlate them. ::: ### Link-level acknowledgement E1381 expects exactly one `ACK` per frame. `autoRespond` answers these at the byte level, before framing and without involving the Bot, which is what lets an analyzer proceed without waiting on a round trip to the server. | Byte | Rule | Acknowledged? | Notes | | -------------- | --------- | ------------- | ----------------------------------------------------------- | | `ENQ` (`0x05`) | `%05:%06` | Yes | Establishment. | | `ETX` (`0x03`) | `%03:%06` | Yes | Terminates the **final** frame of a message. | | `ETB` (`0x17`) | `%17:%06` | Yes | Terminates an **intermediate** frame of a split message. | | `EOT` (`0x04`) | — | **No** | Never acknowledged; the sender has already dropped to idle. | | `LF` (`0x0A`) | — | **No** | Not an ASTM acknowledgement point. | Two mistakes are easy to make here: - **Forgetting `%17:%06`.** Records longer than a frame are split, with every frame but the last terminated by `ETB` rather than `ETX`. Without a rule for `ETB` those frames go unanswered and the analyzer stalls, retransmits, and eventually aborts the session. Short messages work fine, so this often surfaces only once a larger result set arrives. - **Mapping `%0A:%06` or `%04:%06`.** Every frame ends `CR LF`, so a rule on `LF` produces a _second_ `ACK` for each frame. A sender expecting one reply per frame then runs permanently one ahead, reading frame N's `ACK` as the answer to frame N+1 — which means a frame that was actually rejected can be read as accepted. A rule on `EOT` sends an unsolicited `ACK` into the idle state, which stricter analyzers log as a protocol violation. :::caution `autoRespond` cannot validate frames It matches bytes; it does not parse frames or verify checksums, so it `ACK`s a corrupt frame exactly as readily as a good one and can never send a `NAK`. Checksum-driven retransmission is not available through this mechanism. If an analyzer depends on it, validate in the Bot and reconcile out of band. ::: ### What the Bot receives Filtering operates on bytes, not frames, so everything printable in the analyzer's framing survives. A session arrives looking like this: ```text 1H|\^&|||Bio-Rad CDM System SN 12345^...|||||LIS||P|1|20260806041500 P|1||||Doe^John||19700101|M O|1|511979^002^01^0002||^^^4||... R|9|^^^A1c^AREA|5.2|NGSP|... L|1|N D5 ``` Two artifacts to plan for: 1. **Frame sequence numbers.** The `1` ahead of `H` is the frame number. It is an ASCII digit, so `stripControlChars` cannot remove it. 2. **Checksums.** The trailing `D5` is the frame checksum. Splitting on `CR` leaves it on a line of its own, where most parsers harmlessly ignore it. #### Frame packing changes which lines carry a sequence number Only the **first record in each frame** is prefixed with the frame number. An analyzer that packs a whole message into one frame produces exactly one prefix, on the `H` record — so `startsWith('O|1|')` works. The same analyzer splitting that message across frames prefixes the first record of every frame, and the same check silently stops matching. Do not rely on the packing staying constant; it changes with result-set size, retransmissions, and firmware. Normalize instead: ```ts const lines = input .split(/\r\n?/) // Strip a frame sequence number only where one precedes a record type letter. .map((line) => line.replace(/^[0-7](?=[A-Z]\|)/, '')); ``` ### One-way channels Most analyzers send results and never read a reply — the link-level `ACK`s already told them the message was accepted. Set `ignoreResponse=true` so the Bot's response is discarded rather than written back to the socket. Without it, every message writes something to the analyzer, and it is often not what you would expect: when a Bot returns no value, the server substitutes the Bot's **execution logs** as the response body. An analyzer sitting idle then receives kilobytes of unframed log text. :::tip An empty string is a falsy return value, so returning `''` still produces the execution-log fallback. `ignoreResponse=true` is the reliable way to send nothing. ::: ### Sessions with no data An analyzer with nothing to report may still open a session — `ENQ`, take the `ACK`, then `EOT`. Both framing bytes are removed by `stripControlChars`, so what remains is a zero-length body, and the Agent dispatches it to the Bot like any other message. Guard against it at the top of the Bot rather than paying for a full execution per poll: ```ts if (!input?.trim()) { return; } ``` ### Verifying a new channel When bringing up an analyzer, confirm each layer separately — a failure at any one looks like "nothing happened" from the outside: 1. **Link layer.** Watch the `ACK`s. Exactly one per frame, and none after `EOT`. Extra or missing `ACK`s point at the `autoRespond` rules. 2. **Framing.** Log the body's length and whether it contains `CR`. If there is no `CR`, check `keepControlChars`. If the body starts with a control byte instead of a record type letter, check `stripControlChars`. 3. **Parsing.** Log each line and which branch matched it. A `(no match)` on a record you expected to match is usually a frame sequence number, per [Frame packing](#frame-packing-changes-which-lines-carry-a-sequence-number). A Bot that logs these and writes nothing is worth keeping around; channel [logs](./fetch-logs.md) and the Bot's `AuditEvent` history together cover all three layers. --- ## Agent Bulk Status Gets the status of an agent or agents based on given search criteria. Useful for seeing whether agents are connected and listing their current software version. ### Invoke the `$bulk-status` operation ``` [base]/Agent/$bulk-status ``` For example: ```bash medplum get 'Agent/$bulk-status' ``` #### Valid Response The response to this operation is a `Bundle` of `Parameters`. Each `Parameters` within the `Bundle` contains an `agent` and a `result`, which is the result of calling the `$status` operation on this `Agent`, either a `Parameters` or `OperationOutcome` resource. Example response: ```json { "resourceType": "Bundle", "type": "collection", "entry": [ { "resource": { "resourceType": "Parameters", "parameter": [ { "name": "agent", "resource": { "resourceType": "Agent", "name": "Test Agent 1", "status": "active", "id": "93f8b2fb-65a3-4977-a175-71b73b26fde7", "meta": { "versionId": "e182201a-6925-467f-a92b-496193fb4c39", "lastUpdated": "2024-04-19T20:29:25.087Z" } } }, { "name": "result", "resource": { "resourceType": "Parameters", "parameter": [ { "name": "status", "valueCode": "connected" }, { "name": "version", "valueString": "3.1.4" }, { "name": "lastUpdated", "valueInstant": "2024-04-19T00:00:00Z" } ] } } ] } }, { "resource": { "resourceType": "Parameters", "parameter": [ { "name": "agent", "resource": { "resourceType": "Agent", "name": "Test Agent 2", "status": "active", "id": "93f8b2fb-65a3-4977-a175-71b73b26fde7", "meta": { "versionId": "e182201a-6925-467f-a92b-496193fb4c39", "lastUpdated": "2024-04-19T20:29:25.087Z" } } }, { "name": "result", "resource": { "resourceType": "Parameters", "parameter": [ { "name": "status", "valueCode": "disconnected" }, { "name": "version", "valueString": "3.1.2" }, { "name": "lastUpdated", "valueInstant": "2024-04-19T00:00:00Z" } ] } } ] } }, { "resource": { "resourceType": "Parameters", "parameter": [ { "name": "agent", "resource": { "resourceType": "Agent", "name": "Test Agent 3", "status": "off", "id": "93f8b2fb-65a3-4977-a175-71b73b26fde7", "meta": { "versionId": "e182201a-6925-467f-a92b-496193fb4c39", "lastUpdated": "2024-04-19T20:29:25.087Z" } } }, { "name": "result", "resource": { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "exception", "details": { "text": "Something weird happened when getting the status" } } ], } } ] } } ] } ``` #### Invalid Response Example outcome when exceeding max `_count` limit: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "'_count' of 101 is greater than max of 100" } } ] } ``` ### Using search parameters All of the `Agent` search parameters can be used to select which agents to query the status of. Some useful search parameters are: - `name` - `status` - `_count` and `_offset` :::note[Default page size] When `_count` is omitted, the operation queries the status of at most the **default page of 20 agents** — it does _not_ automatically run against every matching agent. The maximum allowed `_count` is `100`. To cover more agents than fit on one page, use `_count` and `_offset` to page through the results (see the paging recipe below). ::: > For more recipes and details on selecting operation targets, see [Using Agent search parameters in bulk operations](./using-search-parameters.md). #### Recipes Getting the status for one agent by name: ```bash medplum get 'Agent/$bulk-status?name=Test+Agent+1' ``` Getting the status of active agents (using the `status=active` search parameter to select the operation targets; without a `_count`, this acts on at most the default page of 20 agents): ```bash medplum get 'Agent/$bulk-status?status=active' ``` Paging through all agent statuses, 50 at a time: ```bash medplum get 'Agent/$bulk-status?_count=50&_offset=0' medplum get 'Agent/$bulk-status?_count=50&_offset=50' ``` --- ## Agent-Side Configuration This guide covers all local configuration options for the Medplum Agent, including how to configure the agent via command line arguments and the `agent.properties` file. ### Configuration Parameters The Medplum Agent requires four essential parameters to connect to your Medplum server: | Parameter | Description | Required | | -------------- | ------------------------------------------------------------- | -------- | | `baseUrl` | The Medplum server base URL (e.g., `https://api.medplum.com`) | Yes | | `clientId` | The OAuth client ID from your ClientApplication | Yes | | `clientSecret` | The OAuth client secret from your ClientApplication | Yes | | `agentId` | The UUID of your Agent resource | Yes | #### Optional Parameters | Parameter | Description | Default | | ------------------ | ---------------------------------------------------------------------------------------- | ------- | | `logLevel` | Global log level for both main and channel loggers | `INFO` | | `logger.main.*` | Configuration for the main logger (see [Logger Configuration](#logger-configuration)) | - | | `logger.channel.*` | Configuration for the channel logger (see [Logger Configuration](#logger-configuration)) | - | ### Configuration Methods There are two ways to configure the Medplum Agent: via command line arguments or using an `agent.properties` file. #### Command Line Arguments When running the agent from the command line, provide the required parameters as arguments: ```bash npm run agent [logLevel] ``` **Example:** ```bash npm run agent https://api.medplum.com my-client-id my-client-secret 123e4567-e89b-12d3-a456-426614174000 ``` **Example with log level:** ```bash npm run agent https://api.medplum.com my-client-id my-client-secret 123e4567-e89b-12d3-a456-426614174000 DEBUG ``` **Limitations:** - Command line arguments only support the four required parameters plus an optional global `logLevel` - The command-line `logLevel` sets the same log level for both main and channel loggers - Advanced logger configuration (separate log directories, file rotation, etc.) is only available via the `agent.properties` file #### Environment Variable Configuration When running the agent in Docker or other containerized environments, you can use the `MEDPLUM_LOG_LEVEL` environment variable to set the global log level: ```bash docker run -e MEDPLUM_LOG_LEVEL="DEBUG" \ -e MEDPLUM_BASE_URL="https://api.medplum.com" \ -e MEDPLUM_CLIENT_ID="my-client-id" \ -e MEDPLUM_CLIENT_SECRET="my-client-secret" \ -e MEDPLUM_AGENT_ID="123e4567-e89b-12d3-a456-426614174000" \ medplum/medplum-agent:latest ``` **Note:** Like command-line arguments, the `MEDPLUM_LOG_LEVEL` environment variable sets the same log level for both main and channel loggers. For granular control over individual logger settings, use the `agent.properties` file. #### Properties File Configuration The `agent.properties` file provides full access to all configuration options, including advanced logger settings. ##### File Location - **Windows**: `C:\Program Files\Medplum Agent\agent.properties` - **Linux/macOS**: In the directory where the agent is installed (typically the same directory as the agent executable) ##### File Format The properties file uses a simple `key=value` format with one setting per line: ```properties baseUrl=https://api.medplum.com clientId=my-client-id clientSecret=my-client-secret agentId=123e4567-e89b-12d3-a456-426614174000 ``` ##### Setting Global Log Level To set the same log level for both main and channel loggers, use the `logLevel` parameter in `agent.properties`: ```properties logLevel=DEBUG ``` This is equivalent to setting both `logger.main.logLevel=DEBUG` and `logger.channel.logLevel=DEBUG`. **Note:** The `logLevel` parameter in `agent.properties` works the same way as: - The optional 5th command-line argument when running the agent directly - The `MEDPLUM_LOG_LEVEL` environment variable when running in Docker If you need different log levels for the main and channel loggers, or want to configure other logger properties (directories, rotation, etc.), use the detailed logger configuration syntax described in the [Logger Configuration](#logger-configuration) section below. ##### Applying Configuration Changes After modifying `agent.properties`, restart the agent service for changes to take effect: - **Windows**: Restart the "Medplum Agent" service from the Services Manager - **CLI/Source**: Stop and restart the agent process ### Logger Configuration The Medplum Agent uses two separate, independently configurable loggers to provide fine-grained control over logging and to protect patient privacy. #### Main Logger vs. Channel Logger ##### Main Logger The **main logger** records general agent activity, including: - Agent startup and shutdown events - Connection status to the Medplum server - Configuration warnings and errors - General system exceptions - Agent lifecycle events **Important:** The main logger does **not** contain Protected Health Information (PHI). ##### Channel Logger The **channel logger** records activity specific to individual communication channels, including: - HL7 message content and processing - DICOM study transfers - ASTM result messages - Channel-specific errors and warnings - Message routing and transformation **Important:** The channel logger **may contain PHI** as it logs the actual content of messages being transmitted. #### Why Separate Loggers? The separation between main and channel loggers serves an important security purpose: it enables **remote log fetching without unnecessarily exposing PHI**. When troubleshooting connection issues or agent configuration problems, administrators can fetch main logger entries to diagnose the issue via the [Fetch Logs FHIR operation](./fetch-logs), without accessing any patient data. Channel logs, which may contain PHI, can be kept with stricter access controls and only accessed when specifically needed for message-level debugging. #### Logger Configuration Properties Each logger (main and channel) supports the following configuration properties: | Property | Type | Default | Description | | --------------- | ------- | --------------- | ---------------------------------------------------------------- | | `logLevel` | string | `INFO` | Log verbosity level: `NONE`, `ERROR`, `WARN`, `INFO`, or `DEBUG` | | `logDir` | string | Agent directory | Directory path where log files are stored | | `maxFileSizeMb` | integer | `10` | Maximum log file size in megabytes before rotation | | `filesToKeep` | integer | `10` | Number of rotated log files to retain | #### Configuration Syntax To configure a logger property, use the format: ``` logger..= ``` Where: - `` is either `main` or `channel` - `` is one of: `logLevel`, `logDir`, `maxFileSizeMb`, `filesToKeep` - `` is the desired setting #### Configuration Examples ##### Example 1: Different Log Levels Set the main logger to `INFO` and increase the channel logger to `DEBUG` for detailed message debugging: ```properties logger.main.logLevel=INFO logger.channel.logLevel=DEBUG ``` ##### Example 2: Separate Log Directories Store main and channel logs in separate directories for easier management and access control: **Linux/macOS:** ```properties logger.main.logDir=/var/log/medplum-agent logger.channel.logDir=/var/log/medplum-agent/channels ``` **Windows:** ```properties logger.main.logDir=C:\Logs\MedplumAgent logger.channel.logDir=C:\Logs\MedplumAgent\Channels ``` ##### Example 3: Different Retention Policies Keep more channel logs for compliance, but limit main logs to conserve space: ```properties logger.main.maxFileSizeMb=10 logger.main.filesToKeep=5 logger.channel.maxFileSizeMb=50 logger.channel.filesToKeep=30 ``` ##### Example 4: Production Configuration A production configuration that separates PHI-containing logs to a separate directory: ```properties baseUrl=https://api.medplum.com clientId=production-client-id clientSecret=production-client-secret agentId=production-agent-id # Main logger: INFO level, moderate retention logger.main.logLevel=INFO logger.main.logDir=/var/log/medplum/main logger.main.maxFileSizeMb=10 logger.main.filesToKeep=10 # Channel logger: WARN level (only warnings and errors), extended retention logger.channel.logLevel=INFO logger.channel.logDir=/var/log/medplum/channels logger.channel.maxFileSizeMb=20 logger.channel.filesToKeep=60 ``` #### Log File Names Logs are written to daily-rotated files with the following naming conventions: - **Main logger**: `medplum-agent-main-YYYY-MM-DD.log` - **Channel logger**: `medplum-agent-channels-YYYY-MM-DD.log` Where `YYYY-MM-DD` is the date the log file was created. **Example file names:** ``` medplum-agent-main-2024-10-02.log medplum-agent-main-2024-10-03.log medplum-agent-channels-2024-10-02.log medplum-agent-channels-2024-10-03.log ``` #### Verifying Logger Configuration After restarting the agent, you can verify the log level is set correctly by examining the log output. Each log entry includes a `level` field in the JSON structure: ```json { "level": "INFO", "msg": "Successfully connected to Medplum server", "timestamp": "2024-10-02T16:52:56.789Z" } ``` For `DEBUG` level logging, you'll see more verbose output: ```json { "level": "DEBUG", "msg": "Received from WebSocket: ...", "timestamp": "2025-10-02T22:43:40.760Z" } ``` #### Resetting to Defaults To reset logger configuration to defaults, remove the corresponding properties from `agent.properties` and restart the agent service. **Default values:** - `logLevel`: `INFO` - `logDir`: Agent installation directory - `maxFileSizeMb`: `10` - `filesToKeep`: `10` ### Best Practices #### Security Considerations 1. **Protect PHI**: Store channel logs in a secure location with appropriate access controls since they may contain PHI 2. **Separate Storage**: Consider storing main and channel logs on different volumes or with different backup policies 3. **Access Control**: Limit access to channel logs to only those who need to troubleshoot message-level issues 4. **Secure Credentials**: Never commit `agent.properties` containing real credentials to version control #### Performance Considerations 1. **Log Level**: Use `INFO` or `WARN` in production; reserve `DEBUG` for troubleshooting 2. **File Size**: Larger `maxFileSizeMb` values reduce rotation frequency but increase memory usage during log queries and decrease logging performance. For more retention, consider increasing `filesToKeep` over `maxFileSizeMb` 3. **Retention**: Balance compliance requirements with disk space 4. **Disk Space**: Monitor log directory disk usage, especially for high-volume channel logging when `filesToKeep` and `maxFileSizeMb` are high #### Operational Recommendations 1. **Monitor Logs**: Regularly review main logger for connection issues and configuration warnings 2. **Rotation**: Ensure log rotation is working by checking for dated log files 3. **Testing**: Test configuration changes in a non-production environment first --- ## Agent Features Each feature that the Agent supports, such as the `Agent/$push` operation, `Agent/$status`, `Agent/$reload-config`, etc. all require minimum versions of both Medplum Server and Medplum Agent in order to work. The matrix of feature, Medplum Server version, and Medplum Agent version looks like this: | Feature | Description | Medplum Server | Medplum Agent | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------------- | | `Prefer: respond-async` | Allows asynchronous calls to `Agent/$push` via the `Prefer` header. The header tells the server to return a `202 Accepted` HTTP response and allows the client to poll for the completion of the `Agent/$push` operation asynchronously without keeping the client waiting for an HTTP response. Useful when a response from a target device can take several seconds or even minutes to come back. | > 3.1.5 | > 3.1.5 | | `Agent/$reload-config` | Allows for dynamically reloading the config of the `Agent` from Medplum Server via the `Agent/$reload-config` FHIR operation. Useful for pushing changes to an Agent via the `Agent` resource without restarting the Agent service. | > 3.1.6 | > 3.1.6 | | `Agent.status` | Allows using `Agent.status` and `Agent.channel.endpoint.status` to disable an Agent or a particular channel via the `Agent` resource. Useful for stopping traffic during Agent maintenance or for debugging of particular channels. | > 3.1.6 | > 3.1.6 | | Agent `keepAlive` setting | Allows you to tell the Agent to keep TCP connections alive for both outgoing traffic (via `Agent/$push`) and incoming traffic (via configured Agent channels) by setting the `keepAlive` setting to `true` on the `Agent` resource. Useful when you want to reduce number of connections to the Agent or if a particular device configuration expects the connection not to close. | > 3.1.9 | > 3.1.10 | | `Agent/$upgrade` | Allows for remotely upgrading the Agent version via the `Agent/$upgrade` FHIR operation. | > 3.1.9 | > 3.1.10 | | Channel `enhanced` mode | Allows agent to acknowledge receipt of a message by enabling **enhanced mode**. In enhanced mode, the Agent will send a [Commit Accept `ACK`](https://hl7-definition.caristix.com/v2/HL7v2.3/Tables/0008) response _before_ the `Bot` has completed processing. Useful when receiving high volumes of messages. Set by adding the `enhanced=true` query parameter in the channel endpoint url (e.g. `mllp://0.0.0.0:2500?enhanced=true`) | > 3.1.9 | > 4.1.8 | | Statistics logging | Allows for enabling periodic stats logging on the agent, which includes stats such as: internal message queue depth, number of active HL7 connections, etc. Set by adding the `logStatsFreqSecs` setting to the `Agent.setting` array, with a valid `valueInteger` value, representing the number of seconds between each statistics log line. | > 3.1.9 | > 4.3.4 | | Channel `messagesPerMin` | Adds a minimum time between processing messages so that at maximum `messagesPerMin` messages can be processed in a minute. Useful when receiving high volumes of messages, especially when using `enhanced` mode. Set by adding the `messagesPerMin=` query parameter in the channel endpoint url (e.g. `mllp://0.0.0.0:2500?enhanced=true&messagesPerMin=60`). Note: Setting `messagesPerMin` tries to smooth out load, and is not just a hard cap on messages per minute. For example, `messagesPerMin=60` will make it so that time between processing messages is at least 1 second. | > 3.1.9 | > 4.3.10 | | `Agent/$fetch-logs` | Allows for remotely fetching the Agent logs via the [`Agent/$fetch-logs` FHIR operation](./fetch-logs). | > 4.3.14 | > 4.3.14 | | Channel `appLevelAck` | Allows configuring application-level acknowledgment behavior for HL7 messages. Controls when application-level ACKs are sent to the remote system based on the ACK code and enhanced mode status. Valid values: `AL` (Always - default), `ER` (Error/Reject only), `NE` (Never), `SU` (Success only). Set by adding the `appLevelAck=` query parameter in the channel endpoint url (e.g. `mllp://0.0.0.0:2500?enhanced=true&appLevelAck=ER`). | > 3.1.9 | > 5.0.4 | | Channel `assignSeqNo` | Allows the Agent to automatically assign sequence numbers to incoming HL7 messages in the MSH-13 field. Useful when the sending system does not provide sequence numbers or when you need consistent sequence numbering. Set by adding the `assignSeqNo=true` query parameter in the channel endpoint url (e.g. `mllp://0.0.0.0:2500?assignSeqNo=true`). | > 3.1.9 | > 5.0.4 | | Agent `maxClientsPerRemote` | Allows configuring the maximum number of concurrent HL7 client connections per remote host. Controls connection pooling behavior when sending messages via `Agent/$push`. Default is 5, or 1 when `keepAlive` is enabled (unless explicitly set). Set by adding the `maxClientsPerRemote` setting to the `Agent.setting` array, with a valid `valueInteger` value. | > 3.1.9 | > 5.0.4 | | AA-only enhanced ACK mode (`enhanced=aa`) | A variant of enhanced mode that sends an [Application Accept (`AA`)](https://hl7-definition.caristix.com/v2/HL7v2.3/Tables/0008) immediately instead of Commit Accept (`CA`). Provides enhanced throughput without requiring the remote system to support or be configured for the two-step enhanced acknowledgement handshake. **Trade-off:** Application-level ACKs (AA/AE/AR) are not forwarded after processing, so the remote system cannot receive asynchronous feedback about processing success or failure. Set by adding the `enhanced=aa` query parameter in the channel endpoint url (e.g. `mllp://0.0.0.0:2500?enhanced=aa`). See [AA Mode documentation](./acknowledgement-modes.md#aa-mode-simplified-enhanced-mode) for details. | > 3.1.9 | > 5.0.11 | | `returnAck` parameter | Allows configuring which ACK message the Agent returns when sending HL7 messages via `Agent/$push`. Valid values: `first` (default): Returns the first ACK message received (e.g., a Commit ACK `CA` when the remote uses enhanced mode) `application`: Waits for and returns the application-level ACK (`AA`, `AE`, or `AR`), skipping any commit-level ACKs Can be configured at two levels: **Per-device default**: Set via the `defaultReturnAck` query parameter on the `Device.url` (e.g., `mllp://192.168.1.100:2575?defaultReturnAck=application`) **Per-message override**: Set via the `returnAck` parameter on `Agent/$push` operation or `pushToAgent()` method Priority order: per-message `returnAck` > device `defaultReturnAck` > `first` (global default) | > 3.1.9 | > 5.0.13 | | `Agent/$stats` operation | Allows for remotely fetching runtime statistics from the Agent via the [`Agent/$stats` FHIR operation](./stats). Includes connection counts, queue depths, RTT metrics, and overall agent health. See also `Statistics logging` for similar functionality without the need for the operation. | > 5.1.9 | > 5.1.9 | | Byte stream channel `autoRespond` | Allows a byte stream channel to answer a link-level handshake itself, writing a fixed byte sequence back to the socket as soon as a configured sequence is seen, without waiting on framing or the `Bot`. Useful for devices that expect an immediate `ACK` (`0x06`) after an `ENQ` (`0x05`) before they will send a message. Set by adding an `autoRespond=:` query parameter in the channel endpoint url (e.g. `tcp://0.0.0.0:9001?startChar=%02&endChar=%03&autoRespond=%05:%06`). Repeat the parameter or comma-separate for more rules; patterns may span several bytes and match across TCP chunk boundaries, per connection. Byte sequences use the same `%XX` form as `startChar`/`endChar`; since percent-decoding is UTF-8, a byte >= `0x80` needs its UTF-8 form (`%C3%A9` for `0xE9`). For the rules an ASTM analyzer expects, see [ASTM Channels](./astm-channels.md#link-level-acknowledgement). | Any | > 5.1.28 | | Byte stream channel body filtering | Allows a byte stream channel to clean up a message body before it is sent to the `Bot`. `stripSequence=` removes every whole occurrence of a byte sequence; repeat the parameter or comma-separate for more `stripControlChars=true` removes any remaining C0 control byte (`0x00`-`0x1F`), including the `startChar`/`endChar` framing `keepControlChars=` exempts bytes from that sweep. Record-oriented protocols need their terminators to survive it — an ASTM body stripped of `CR` is one run-on line the receiver can no longer split into records, so such a channel wants `keepControlChars=%0D%0A`. `bodyEncoding=hex` (default) or `utf-8` controls how the body is encoded on the wire to and from the server Set via query parameters in the channel endpoint url (e.g. `tcp://0.0.0.0:9004?startChar=%05&endChar=%04&stripControlChars=true&keepControlChars=%0D%0A&bodyEncoding=utf-8`). All are off by default, so existing channels keep sending hex-encoded bodies with their framing intact. Filtering works on bytes, not on frames, so anything printable in a device's framing still reaches the `Bot`. An ASTM body arrives carrying each frame's leading sequence number and trailing checksum, which the `Bot` has to strip itself — see [ASTM Channels](./astm-channels.md#what-the-bot-receives). | Any | > 5.1.28 | | Byte stream channel `ignoreResponse` | Makes a byte stream channel one-way: the `Bot`'s response is discarded instead of being written back to the device socket. Messages still reach the `Bot` as usual, and `autoRespond` still answers link-level handshakes — only the response leg is dropped. Use it for devices that never read a reply, such as an analyzer that considers a session complete once its own link-level exchange ends. Without it every message draws a write back to the device, and a `Bot` that returns nothing draws the server's fallback body — the `Bot`'s own execution logs — which arrives at the device as unframed junk. Set by adding `ignoreResponse=true` in the channel endpoint url (e.g. `tcp://0.0.0.0:9004?startChar=%05&endChar=%04&ignoreResponse=true`). Off by default. | Any | > 5.1.28 | | DICOM channel `storage=dicomweb` | Sends each instance a DICOM channel receives via C-STORE to the server's DICOMweb STOW-RS endpoint (`POST /dicomweb/studies`), which files it into `DicomStudy`, `DicomSeries`, and `DicomInstance` resources, instead of uploading it as a FHIR `Binary`. Set by adding the `storage=dicomweb` query parameter in the channel endpoint url (e.g. `dicom://0.0.0.0:8104?storage=dicomweb`). Defaults to `storage=binary`, which uploads a `Binary` and includes a reference to it in the `Bot` payload, so existing DICOM channels are unaffected. In `dicomweb` mode the payload has no `binary` field — the study is addressed through the DICOMweb resources the server created instead. Requires a server with DICOMweb support; against an older server the STOW-RS request 404s and the C-STORE fails with a processing failure status. | > 5.1.27 | > 5.1.28 | | Agent `durableQueue` | Routes inbound HL7 messages through an on-disk SQLite queue on the Agent before they reach the `Bot`. The commit `ACK` (`CA`, or `AA` in AA mode) is sent only once the message is committed to disk, so an ACKed message survives an Agent crash, restart, or upgrade; interrupted rows are recovered on startup. Set by adding the `durableQueue` setting to the `Agent.setting` array with `valueBoolean: true`. Off by default. Related settings: `queueDbPath`, `queueRetentionDays`, `queueRetentionMaxMb`, `queueErroredRetentionDays`, `queueSweepIntervalSecs`. Required for auto-retry and for logical channels. Also enables the `duplicateBehavior` channel parameter (`idempotent` / `reject`) for repeated `MSH-10` values. See the [High Throughput HL7 guide](./high-throughput-hl7.md#solution-3-the-durable-queue--makes-the-fast-ack-honest). | Any | > 5.1.22 | | Channel auto-retry | Retries queue-to-`Bot` delivery with exponential backoff and jitter when a message fails. Modes: `guaranteed` (default — retry indefinitely until upstream answers, accepting possible duplicate delivery), `normal` (transient failures only, capped at 10 attempts), and `none`. Set agent-wide via the `channelRetryMode`, `channelAutoRetryBaseDelayMs`, `channelAutoRetryMaxDelayMs`, `channelAutoRetryMaxAttempts`, and `channelAutoRetryBackoffMultiplier` settings, or per channel via the `retryMode`, `autoRetryBaseDelayMs`, `autoRetryMaxDelayMs`, `autoRetryMaxAttempts`, and `autoRetryBackoffMultiplier` endpoint url parameters, which override the agent-wide settings field by field. Requires `durableQueue`; without it the retry settings have no effect and the Agent warns. | Any | > 5.1.25 | | Channel logical channels | Partitions a single HL7 channel's queue into independent FIFO streams keyed on message content, so unrelated messages process concurrently instead of queueing behind each other. Order is strict within a partition and unconstrained across partitions — a feed keyed on patient ID keeps each patient's messages ordered while draining many patients in parallel. Set the key via the `channelLogicalChannelKey` agent setting or the `logicalChannelKey` endpoint url parameter, as comma-separated HL7 field paths in `SEGMENT-field[.component[.subcomponent]]` notation (e.g. `PID-3.1` or `MSH-4,MSH-9.2`). Set the concurrency via the `channelMaxWorkers` setting or the `maxWorkers` parameter (default 1, max 500), e.g. `mllp://0.0.0.0:9001?enhanced=true&logicalChannelKey=PID-3.1&maxWorkers=64`. Requires `durableQueue`; without it both have no effect and the Agent warns. See the [High Throughput HL7 guide](./high-throughput-hl7.md#solution-4-logical-channels--the-actual-throughput-fix). | Any | > 5.1.29 | ### Major Bug Fixes | Server Version | Agent Version | Description | Related PR | | -------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | | 3.2.10 | N/A | Fixes HL7 ACK messages from bots not making it back to the sending device. | [5212](https://github.com/medplum/medplum/pull/5212) | | < 3.2.10 | 4.1.9 | Reverted backwards-incompatible change requiring `callback` field for `agent:transmit:response` messages. | [6793](https://github.com/medplum/medplum/pull/6793) | | N/A | 5.0.13 | Reverted default `returnAck` behavior back to `first` for backwards compatibility. Previously changed to `application`, which broke workflows expecting the first ACK (e.g., Commit ACK `CA`) to be returned immediately. | [8279](https://github.com/medplum/medplum/pull/8279) | | N/A | 5.1.15 | Improves the reliability of the `Agent/$upgrade` process. A behavior change in the underlying `@medplum/hl7` library in v5 introduced a downstream ordering issue during upgrade: the new Agent version would install successfully, but the new service could stall while waiting to acquire its ports because the old service had not yet released them. In practice this required manually stopping the old service to trigger cleanup and complete the upgrade. The shutdown of the old Agent is now signaled before the new Agent waits on its channels, allowing ports to be released in the correct order and making the upgrade process significantly more stable. | [9382](https://github.com/medplum/medplum/pull/9382) | ### Compatibility Between Versions Aside from the features and bug fixes listed above, the majority of the core functionality of the Medplum Agent is broadly compatible across all versions; notably from `3.1.5` onward. This means the `Agent/$push` FHIR operation functions broadly the same across versions and should be compatible on Medplum Server and Medplum Agent versions `> 3.1.5`. However, it is not advised to run older versions of the Medplum Agent or Medplum Server against each other if possible, and **we recommend to regularly update both Medplum Server and Medplum Agent in tandem.** --- ## Agent Fetch Logs Fetches log entries from a given agent or agents. Useful for debugging and monitoring agent behavior. ### Invoke the `$fetch-logs` operation #### Single Agent ``` [base]/Agent/[id]/$fetch-logs ``` For example: ```bash medplum get 'Agent/[id]/$fetch-logs' ``` #### Multiple Agents ``` [base]/Agent/$fetch-logs ``` For example: ```bash medplum get 'Agent/$fetch-logs?_tag=Group+A' ``` Here, `_tag=Group+A` is an `Agent` search parameter used to select which agents to fetch logs from. For example, to fetch logs from all active agents use `status=active`. See [Using Agent search parameters in bulk operations](./using-search-parameters.md) for more ways to select operation targets. ### Parameters - `limit` (optional; default: `20`): Maximum number of log entries to return per agent > The parameter above configures _how_ logs are fetched. To select _which_ agents to fetch logs from when using the multi-agent endpoint, use `Agent` search parameters — see [Using Agent search parameters in bulk operations](./using-search-parameters.md). ### Single Agent Response When querying a single agent by ID, the response is a `Parameters` resource containing the logs. #### Valid Response Example response when logs are successfully retrieved: ```json { "resourceType": "Parameters", "parameter": [ { "name": "logs", "valueString": "{\"level\":\"INFO\",\"timestamp\":\"2024-04-19T00:00:00.000Z\",\"msg\":\"Agent started successfully\"}\n{\"level\":\"INFO\",\"timestamp\":\"2024-04-19T00:01:00.000Z\",\"msg\":\"Processing message\"}\n{\"level\":\"ERROR\",\"timestamp\":\"2024-04-19T00:02:00.000Z\",\"msg\":\"An error occurred\",\"error\":\"Error: Connection failed\"}" } ] } ``` The `logs` parameter contains a newline-separated string of JSON objects, where each object represents a log message with: - `level`: Log level (`ERROR`, `WARN`, `INFO`, `DEBUG`) - `timestamp`: ISO 8601 timestamp - `msg`: Log message - Additional fields may be present (e.g., `error` for error logs) ### Multi-Agent Response When querying multiple agents, the response is a `Bundle` of `Parameters`. Each `Parameters` within the `Bundle` contains an `agent` and a `result`, which is the result of calling the `$fetch-logs` operation on this `Agent`, either a `Parameters` or `OperationOutcome` resource. #### Valid Response Example response for multiple agents: ```json { "resourceType": "Bundle", "type": "collection", "entry": [ { "resource": { "resourceType": "Parameters", "parameter": [ { "name": "agent", "resource": { "resourceType": "Agent", "name": "Test Agent 1", "status": "active", "id": "93f8b2fb-65a3-4977-a175-71b73b26fde7", "meta": { "versionId": "e182201a-6925-467f-a92b-496193fb4c39", "lastUpdated": "2024-04-19T20:29:25.087Z" } } }, { "name": "result", "resource": { "resourceType": "Parameters", "parameter": [ { "name": "logs", "valueString": "{\"level\":\"INFO\",\"timestamp\":\"2024-04-19T00:00:00.000Z\",\"msg\":\"Agent started\"}\n{\"level\":\"INFO\",\"timestamp\":\"2024-04-19T00:01:00.000Z\",\"msg\":\"Processing message\"}" } ] } } ] } }, { "resource": { "resourceType": "Parameters", "parameter": [ { "name": "agent", "resource": { "resourceType": "Agent", "name": "Test Agent 2", "status": "active", "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890", "meta": { "versionId": "f293201a-6925-467f-a92b-496193fb4c39", "lastUpdated": "2024-04-19T20:29:25.087Z" } } }, { "name": "result", "resource": { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "exception", "details": { "text": "Failed to fetch logs: Agent not responding" } } ] } } ] } } ] } ``` #### Invalid Response Example outcome when an ID was not supplied to the operation: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "Must specify agent ID or identifier" } } ] } ``` Example outcome when exceeding max `_count` limit: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "'_count' of 101 is greater than max of 100" } } ] } ``` ### Using search parameters All of the `Agent` search parameters can be used to select which agents to query logs from when using the multi-agent endpoint. Some useful search parameters are: - `name` - `status` - `_count` and `_offset` :::note[Default page size] When `_count` is omitted, the operation fetches logs from at most the **default page of 20 agents** — it does _not_ automatically run against every matching agent. The maximum allowed `_count` is `100`. To cover more agents than fit on one page, use `_count` and `_offset` to page through the results (see [Paging Through Agent Logs](#paging-through-agent-logs)). ::: ### Recipes #### Single Agent by ID Fetch logs from a specific agent by ID: ```bash medplum get 'Agent/93f8b2fb-65a3-4977-a175-71b73b26fde7/$fetch-logs' ``` #### Single Agent by ID with Limit Fetch the last 10 log entries from a specific agent: ```bash medplum get 'Agent/93f8b2fb-65a3-4977-a175-71b73b26fde7/$fetch-logs?limit=10' ``` #### Single Agent by Identifier Fetch logs from a specific agent by identifier: ```bash medplum get 'Agent/$fetch-logs?identifier=agent-007' ``` #### Multiple Agents by Name Fetch logs from agents with a specific name prefix (without a `_count`, this acts on at most the default page of 20 agents): ```bash medplum get 'Agent/$fetch-logs?name=Production+Agent' ``` #### Multiple Agents by Status Fetch logs from active agents (without a `_count`, this acts on at most the default page of 20 agents): ```bash medplum get 'Agent/$fetch-logs?status=active' ``` #### Multiple Agents with Limit Fetch logs from the matched agents, limited to 5 entries per agent (without a `_count`, this acts on at most the default page of 20 agents): ```bash medplum get 'Agent/$fetch-logs?limit=5' ``` #### Paging Through Agent Logs Fetch logs from agents in batches, 50 agents at a time: ```bash medplum get 'Agent/$fetch-logs?_count=50&_offset=0' medplum get 'Agent/$fetch-logs?_count=50&_offset=50' ``` --- ## High Throughput HL7 Channel Configuration This is the end-to-end guide to configuring a Medplum Agent HL7 channel for a high-volume feed: what actually limits throughput, which knob addresses which limit, and a reference configuration you can copy. ### What are the bottlenecks of high throughput HL7 connections? A "slow" HL7 interface is almost never slow because of the network. A single MLLP socket on a LAN can move tens of thousands of small messages per second. The bottleneck is the **protocol's request/response discipline**, and it shows up in three places. #### 1. The connection is a single-threaded, strictly-ordered pipe In [Original Acknowledgement Mode](./acknowledgement-modes.md#original-acknowledgement-mode), the sender transmits one message and then **blocks on that socket** until it receives an application ACK. Nothing else moves on that connection until the receiver has finished its entire processing pipeline — parse, validate, business logic, database commits, downstream calls. That makes end-to-end throughput a direct function of per-message processing latency: ``` throughput ≈ 1 / (processing latency + round trip) ``` At 250 ms of Bot processing, that's 4 msg/sec. Not 4 msg/sec per stream — 4 msg/sec for the **entire interface**. Adding CPU, memory, or bandwidth changes nothing, because none of those are the constraint. The constraint is that the protocol permits exactly one message in flight. #### 2. A "single feed" is really many independent streams, multiplexed onto one wire This is the part that matters most, and it's the part HL7v2 gives you no vocabulary for. A hospital ADT feed is not one stream of causally-related events. It's the interleaving of thousands of unrelated streams: ``` MSH … ADT^A08 PID|…|MRN-00412|… ← patient A, admit update MSH … ORU^R01 PID|…|MRN-98773|… ← patient B, lab result MSH … ADT^A03 PID|…|MRN-00412|… ← patient A, discharge (must follow A's A08) MSH … ADT^A01 PID|…|MRN-55019|… ← patient C, admission MSH … ORU^R01 PID|…|MRN-98773|… ← patient B, corrected result (must follow B's first) ``` Ordering genuinely matters **within** a patient: an A08 update applied after an A03 discharge, or a corrected lab result applied before the original, produces a wrong chart. Ordering is almost always irrelevant **between** patients: nothing about patient C's admission depends on patient A's discharge. So the true ordering requirement of the feed is a set of thousands of small independent FIFO queues. What the protocol gives you is **one** FIFO queue, and no way to say which messages belong to which stream. Every message waits behind every other message, including thousands it has no relationship to whatsoever. That's the core mismatch: **HL7v2's single-connection FIFO semantics are strictly stronger than the ordering the data actually requires, and that excess strength is paid for entirely in throughput.** #### 3. Backpressure is not durability The blocking ACK does buy something real: the sender knows the message was handled, and holds it until then. Any solution that unblocks the sender has to replace that guarantee with something else, or it converts a throughput problem into a data-loss problem. Any serious answer here has to solve throughput **and** durability together. --- ### The solutions, and what each one actually buys #### Solution 1: Enhanced ACK mode — unblocks the sender [Enhanced Acknowledgement Mode](./acknowledgement-modes.md) splits the single ACK into two: a fast **Commit Accept (CA)** sent as soon as the message is safely stored, and a later **Application ACK (AA/AE/AR)** sent once processing actually finishes. The socket is released at the CA, so the sender can transmit the next message immediately. ``` mllp://0.0.0.0:9001?enhanced=true ``` **What this fixes:** the sender is no longer blocked on your processing latency. Messages arrive as fast as the sender can push them. **What this does not fix:** how fast _you_ drain them. Enhanced mode moves the queue from "in the sender's outbox" to "in your queue." If your channel still processes one message at a time, you have the same 4 msg/sec drain rate — you've just built a backlog faster. Under sustained load the queue grows without bound. Enhanced mode is necessary but not sufficient. **It only becomes a throughput win when you can drain the queue in parallel.** #### Solution 2: AA mode — enhanced throughput when you can't configure the peer Standard enhanced mode requires the _sender_ to be configured for the two-step handshake (MSH-15/MSH-16). Frequently you can't do that: the sender is a partner's or client's Mirth Connect instance, a vendor appliance, or a legacy system whose interface team is a change-request away. [AA Mode](./acknowledgement-modes.md#aa-mode-simplified-enhanced-mode) solves this. The Agent immediately returns a plain **Application Accept (AA)** — an ordinary code every HL7 system already understands — and processes asynchronously. ``` mllp://0.0.0.0:9001?enhanced=aa ``` **Use AA mode when:** - You're receiving from a partner's or client's Mirth (or any third-party interface engine) and they can't easily enable enhanced ACK mode on their sending channel. - You have no control over the remote system's configuration at all. - Your workflow doesn't need application-level failure feedback delivered back over the wire. **The trade-off:** the AA is sent before processing, so you can never send back an AE or AR. The sender always sees success. You must handle processing failures on your side — which is exactly what the durable queue and its retry policy are for (below). See the [AA Mode documentation](./acknowledgement-modes.md#aa-mode-simplified-enhanced-mode) for full details. #### Solution 3: The durable queue — makes the fast ACK honest Added in **Medplum Agent version 5.1.22**; auto-retry in **5.1.25**. Enabling `durableQueue` puts a local SQLite queue on the Agent between the MLLP socket and the Bot. ```json { "name": "durableQueue", "valueBoolean": true } ``` With the queue on, the Agent takes ownership of the commit ACK and sends CA (or AA) **only after the message is committed to disk**. A message that has been ACKed is on durable storage and survives an Agent crash, a restart, or an upgrade; on startup the Agent recovers interrupted rows and resumes. The queue is also the prerequisite for everything that follows: retries, and parallel draining. `maxWorkers` and `logicalChannelKey` have no effect without it, and the Agent logs a warning if you set them with the queue off. The queue additionally gives you: - **Auto-retry with exponential backoff and jitter.** Default mode is `guaranteed` — retry indefinitely until upstream answers, accepting possible duplicate delivery. `normal` retries only transient failures, capped at 10 attempts. `none` disables retry. - **Duplicate handling.** Same `(channel, MSH-10)` arriving while a prior row is still in flight is handled per `duplicateBehavior`: `idempotent` (default — replay the prior ACK when the bytes match) or `reject`. - **Retention.** A sweeper prunes terminal rows on time and size bounds. #### Solution 4: Logical channels — the actual throughput fix Added in **Medplum Agent version 5.1.29**. This is the piece that addresses bottleneck #2 directly. HL7v2 has no notion of a stream identifier — there is no "partition key" field, no session, no correlation ID. But the information is right there in the message: for most feeds, _the patient identifier is the stream identifier_. Messages about different patients are independent, and messages about the same patient must stay ordered. So Medplum lets you declare it. A **logical channel key** is a spec naming one or more HL7 fields; the Agent computes the key from each message and uses it to partition the queue: ``` mllp://0.0.0.0:9001?enhanced=true&logicalChannelKey=PID-3.1&maxWorkers=64 ``` One physical MLLP channel becomes N independent logical channels — one per distinct key value — each preserving strict FIFO order internally, all draining concurrently. **Ordering guarantee:** - **Within a logical channel:** strict FIFO, exactly as before. Two messages with the same key never process concurrently or out of order. - **Across logical channels:** no ordering. That's the point. **How it works.** A row's partition is computed at _claim_ time, not at intake, so it can never go stale across retries, requeues, restarts, or a spec change. A single per-channel dispatcher claims the lowest-id queued row, computes its key synchronously, and checks whether an earlier message in that same partition is still unsettled. If so, the row is parked as `delayed` and woken when its blocker settles; otherwise it goes straight to a free worker. Because the claim-and-gate is one synchronous critical section with exactly one claimer, two messages of the same logical channel can never be dispatched concurrently. **Key spec syntax.** Comma-separated HL7 field paths in conventional notation, `SEGMENT-field[.component[.subcomponent]]`, all 1-based: | Spec | Partitions by | Use when | | :------------------ | :------------------------------- | :------------------------------------------------------------ | | `PID-3.1` | Patient ID | The common case — ADT, ORU, ORM feeds keyed on the patient | | `PID-3.1,PID-3.4` | Patient ID + assigning authority | MRNs are only unique within an issuing facility | | `MSH-4` | Sending facility | Coarse partitioning across a multi-facility feed | | `MSH-4,MSH-9.2` | Sending facility + trigger event | Isolate slow message types from fast ones | | `PV1-19` | Visit number | Ordering is per-encounter rather than per-patient | | _(empty — default)_ | Nothing; one serialized queue | Ordering is globally required, or you haven't validated a key | Validation is all-or-nothing: if any token is malformed the whole spec is rejected with a warning and the previous spec stays in effect, so a typo degrades to your last-known-good partitioning rather than to a wrong one. A message missing the addressed field contributes an empty value — all such messages share one partition, so pick a field your feed reliably populates. **Choosing a key is a clinical-safety decision, not just a performance one.** The correctness requirement is: _any two messages that must be applied in order must produce the same key._ Before deploying, check for cross-patient dependencies in your feed — merge/link messages (`ADT^A18`, `ADT^A40`), messages that reference a prior message's assigned identifier, and any Bot logic that reads state written by a different patient's message. When in doubt, partition coarser (`MSH-4`) or not at all. #### Solution 5: Worker pool sizing `maxWorkers` sets how many messages a channel may have in flight at once. Each worker holds exactly one message and does nothing but await the server round trip — it's I/O-bound, not CPU-bound — so the pool size you want is roughly: ``` maxWorkers ≈ target throughput (msg/sec) × per-message latency (sec) ``` At a 300 msg/sec target and 200 ms per message, that's 60 in flight; round to **64**. Overshooting is cheap (idle workers cost almost nothing); undershooting caps you below the target. The value is clamped to a maximum of **500**. Note that pool size does not multiply claim cost: a single dispatcher owns the claim side, issuing one claim per enqueue and one per freed worker slot regardless of `maxWorkers`. Two constraints on the pool: - `maxWorkers > 1` requires `durableQueue: true`. Without it the setting is ignored and the Agent warns. - `maxWorkers > 1` combined with `assignSeqNo=true` warns: sequence numbers are assigned in arrival order, but delivery across partitions is concurrent, so upstream will see them out of order. #### Solution 6: Outbound connection pooling (`keepAlive` + `maxClientsPerRemote`) Everything above concerns inbound traffic. If your Bots also push HL7 downstream via [`Agent/$push`](./push.md), the outbound side has its own version of the same bottleneck: one connection to a remote host means one message in flight to it. - **`keepAlive: true`** reuses TCP connections instead of dialing per message, removing connection setup from the per-message path. - **`maxClientsPerRemote`** sets how many concurrent client connections the Agent maintains per remote host. Default is **5**, but it drops to **1** when `keepAlive` is enabled unless you set it explicitly — so if you enable `keepAlive` for a high-volume outbound feed, **set `maxClientsPerRemote` explicitly** or you will silently serialize. A value of **25** is a reasonable starting point for a high-volume outbound feed, paired with a parallel inbound pool. As with `maxWorkers`, size it to `throughput × latency`, and confirm the receiving system will accept that many concurrent connections — many interface engines cap inbound connections per source. #### Solution 7: Rate limiting (`messagesPerMin`) Sometimes the goal is not maximum throughput but _survivable_ throughput. If a downstream system or your own database can't absorb a burst, `messagesPerMin` smooths the flow by enforcing a minimum interval between processed messages. This pairs naturally with enhanced mode: accept fast, drain at a controlled rate. --- ### Reference configuration An `Agent` configured for a high-volume feed: durable queue on, partitioned by patient ID, 64 workers inbound, keep-alive with 25 outbound connections per remote. Requires **Medplum Agent version 5.1.29** or later. ```json { "resourceType": "Agent", "name": "High Throughput Agent", "status": "active", "setting": [ { "name": "keepAlive", "valueBoolean": true }, { "name": "durableQueue", "valueBoolean": true }, { "name": "channelMaxWorkers", "valueInteger": 64 }, { "name": "channelLogicalChannelKey", "valueString": "PID-3.1" }, { "name": "maxClientsPerRemote", "valueInteger": 25 }, { "name": "logStatsFreqSecs", "valueInteger": 60 } ], "channel": [ { "name": "inbound-hl7", "endpoint": { "reference": "Endpoint/00000000-0000-0000-0000-000000000000", "display": "Inbound HL7 Endpoint" }, "targetReference": { "reference": "Bot/00000000-0000-0000-0000-000000000000", "display": "Inbound HL7 Bot" } } ] } ``` The `channelMaxWorkers` and `channelLogicalChannelKey` settings are **agent-wide defaults** applied to every HL7 channel. Any channel can override them with endpoint URL parameters — useful when one channel carries a feed you haven't validated a partition key for. The matching `Endpoint.address`, using standard enhanced mode: ```json { "resourceType": "Endpoint", "status": "active", "connectionType": { "system": "http://terminology.hl7.org/CodeSystem/endpoint-connection-type", "code": "hl7v2-mllp", "display": "HL7 v2 MLLP" }, "name": "Inbound HL7 Endpoint", "payloadType": [ { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/endpoint-payload-type", "code": "any", "display": "Any" } ] } ], "address": "mllp://0.0.0.0:9001?enhanced=true" } ``` Or, when the sending system can't be configured for the two-step handshake (a partner's Mirth, a vendor appliance): ``` mllp://0.0.0.0:9001?enhanced=aa ``` Per-channel overrides of the agent-wide defaults go on the same URL: ``` mllp://0.0.0.0:9001?enhanced=true&logicalChannelKey=MSH-4&maxWorkers=16 ``` --- ### Configuration reference #### Agent settings (`Agent.setting`) Minimum Agent versions: `durableQueue` and the `queue*` settings **5.1.22**; `channelRetryMode` and the `channelAutoRetry*` settings **5.1.25**; `channelMaxWorkers` and `channelLogicalChannelKey` **5.1.29**. See the [feature matrix](./features.md) for the rest. | Setting | Type | Default | Purpose | | :---------------------------------- | :-------- | :------------------------------------ | :-------------------------------------------------------------------------------------------------------- | | `durableQueue` | `boolean` | `false` | Master switch for the on-disk queue. Required for retries, `maxWorkers`, and `logicalChannelKey`. | | `channelMaxWorkers` | `integer` | `1` | Default in-flight messages per HL7 channel. Clamped to 500. | | `channelLogicalChannelKey` | `string` | _(none)_ | Default partition-key spec for HL7 channels, e.g. `PID-3.1`. | | `keepAlive` | `boolean` | `false` | Reuse TCP connections for inbound and outbound traffic. | | `maxClientsPerRemote` | `integer` | `5` (`1` if `keepAlive`) | Concurrent outbound client connections per remote host. | | `logStatsFreqSecs` | `integer` | _(off)_ | Emit a periodic stats log line every N seconds. | | `queueDbPath` | `string` | `/medplum-agent-queue.sqlite` | Queue DB location. Cannot be changed while the queue is open — disable, then re-enable with the new path. | | `queueRetentionDays` | `integer` | `7` | How long `processed` rows are kept. | | `queueRetentionMaxMb` | `integer` | `512` | Soft cap on queue DB size, in MiB. | | `queueErroredRetentionDays` | `integer` | `90` | Floor on retention for `errored` / `nacked` rows. | | `queueSweepIntervalSecs` | `integer` | `3600` | How often the retention sweeper runs. | | `channelRetryMode` | `string` | `guaranteed` | Default retry mode: `none`, `normal`, or `guaranteed`. | | `channelAutoRetryBaseDelayMs` | `integer` | `1000` | First backoff delay. | | `channelAutoRetryMaxDelayMs` | `integer` | `60000` | Backoff ceiling. | | `channelAutoRetryMaxAttempts` | `integer` | `0` (unlimited) / `10` in `normal` | Attempt cap. Setting a nonzero value under `guaranteed` warns and voids the guarantee. | | `channelAutoRetryBackoffMultiplier` | `decimal` | `2` | Exponential backoff multiplier. | #### Endpoint URL parameters (`Endpoint.address`) Per-channel; where both exist, the URL parameter wins over the agent-wide setting, field by field. Minimum Agent versions: `duplicateBehavior` **5.1.22**; the `retryMode` / `autoRetry*` params **5.1.25**; `logicalChannelKey` and `maxWorkers` **5.1.29**. See the [feature matrix](./features.md) for the rest. | Parameter | Values | Default | Purpose | | :--------------------------- | :----------------------------- | :------------ | :----------------------------------------------------------------------- | | `enhanced` | `true`, `aa` | _(off)_ | `true` = standard two-step CA/AA; `aa` = immediate AA, no async app ACK. | | `logicalChannelKey` | HL7 field spec | _(none)_ | Partition key, e.g. `PID-3.1` or `MSH-4,MSH-9.2`. | | `maxWorkers` | `1`–`500` | `1` | In-flight messages for this channel. | | `messagesPerMin` | integer | _(unlimited)_ | Smooths processing to at most N messages per minute. | | `appLevelAck` | `AL`, `NE`, `ER`, `SU` | `AL` | Which application ACKs to forward in standard enhanced mode. | | `duplicateBehavior` | `idempotent`, `reject` | `idempotent` | Handling of a repeated `MSH-10` while a prior row is in flight. | | `retryMode` | `none`, `normal`, `guaranteed` | `guaranteed` | Retry aggressiveness for queue → Bot delivery. | | `autoRetryBaseDelayMs` | integer ≥ 1 | `1000` | First backoff delay. | | `autoRetryMaxDelayMs` | integer ≥ 1 | `60000` | Backoff ceiling. | | `autoRetryMaxAttempts` | integer ≥ 0 | `0` / `10` | Attempt cap (`0` = unlimited). | | `autoRetryBackoffMultiplier` | number ≥ 1 | `2` | Exponential backoff multiplier. | | `encoding` | e.g. `utf-8`, `latin1` | `utf-8` | Wire encoding. | | `assignSeqNo` | `true`, `false` | `false` | Assign sequence numbers. Warns when combined with `maxWorkers > 1`. | --- ### Rollout and verification Order matters — each step depends on the one before it. 1. **Turn on the durable queue first, alone.** `durableQueue: true` with `maxWorkers` still at 1. Nothing about throughput changes; you're establishing durability and confirming the queue DB has somewhere persistent to live. Verify the DB file appears and the Agent logs that it acquired the queue lease. 2. **Enable enhanced mode.** `enhanced=true` if the sender supports the two-step handshake, `enhanced=aa` if it doesn't. Confirm the sender is satisfied with the ACKs it receives. At this point the sender is unblocked but you're still draining serially — expect the queue to build under load. That's the expected intermediate state, not a failure. 3. **Validate your partition key against real traffic before enabling it.** Sample a representative window of your feed and confirm every ordering dependency you care about falls within a single key value. This is the step that protects data correctness; don't skip it. 4. **Set `logicalChannelKey`, then raise `maxWorkers`.** Both, together — either alone is the wrong configuration. Start conservative (8–16), watch queue depth drain, then raise toward `throughput × latency`. 5. **Size the outbound pool** if your Bots push downstream: `keepAlive: true` plus an explicit `maxClientsPerRemote`. **What to watch.** [`Agent/$stats`](./stats.md) reports connection counts, queue depths, and RTT metrics on demand; `logStatsFreqSecs` writes the same picture to the Agent log on an interval. The signal that matters is **queue depth over time**: flat or draining means the pool is keeping up; monotonically rising means you are still accepting faster than you drain, and either `maxWorkers` is too low, your partition key is too coarse (too few distinct values to parallelize across), or the real constraint has moved downstream into the Bot or the server. If raising `maxWorkers` stops helping, check the key's cardinality first. A key that yields only a handful of distinct values — `MSH-4` on a single-facility feed, for example — caps your effective concurrency at that number no matter how large the pool is. ### See also - [Acknowledgement Modes](./acknowledgement-modes.md) — full treatment of Original, Enhanced, and AA modes - [Agent Features](./features.md) — feature/version compatibility matrix - [`Agent/$stats`](./stats.md) — runtime statistics - [`Agent/$push`](./push.md) — outbound message delivery - [Troubleshooting](./troubleshooting.md) --- ## Intro to Medplum Agent The Medplum Agent is an application that runs inside your firewall and connects to devices over low level protocols such as HL7/MLLP, ASTM, and DICOM. These network protocols are commonly unencrypted, and therefore require an adapter to a secure transport. The Medplum Agent uses secure HTTPS WebSocket channels to stream messages between devices and a Medplum server cluster in the cloud. ### How it works The Medplum Agent runs in your local network and connects to devices over low level protocols: ![Medplum Agent Overview](./medplum-agent-overview.webp) The Medplum Agent securely connects to Medplum servers running in the cloud using HTTPS and WebSockets: ![Medplum Agent Connection Diagram](./medplum-agent-connection-diagram.webp) ### Installation The installation of the agent has two parts: first [setting up the configurations on the Medplum app](#configuration-on-medplum-app), then [installing the application on the edge host](#install-on-host) and then inputting the configurations from the first step into the installer. After completing your install you'll be able to [send test transmissions end-to-end](#testing-your-setup). #### Configuration on Medplum App The Medplum App setup includes creating four resources which represent the configuration of the agent itself. These resources are `Endpoint`, `Bot`, `Agent`, and `ClientApplication`. ##### Set up an Endpoint Resource First, set up the [Endpoint](/docs/api/fhir/resources/endpoint). An endpoint is a representation of what the on-site system will connect to. For visibility you may consider adding a `managingOrganization`, `address`, `contact`, and related metadata the `Endpoint` to represent where this physical on-site system is. Here is the link to create a [new Endpoint](https://app.medplum.com/Endpoint/new) on the Medplum App. Assuming you want to configure the Endpoint to listen to HL7 v2 over MLLP you'll want set up agent as follows: ```json { "resourceType": "Endpoint", "status": "active", "connectionType": { "system": "http://terminology.hl7.org/CodeSystem/endpoint-connection-type", "code": "hl7v2-mllp", "display": "HL7 v2 MLLP" }, "name": "Test Endpoint", "payloadType": [ { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/endpoint-payload-type", "code": "any", "display": "Any" } ] } ], "address": "mllp://0.0.0.0:56000" } ``` ##### Set up a Bot Resource Second, set up the [Bot](https://app.medplum.com/Bot/new) that accepts the HL7 message, applies necessary transformations and translations, and sends an `ACK` message back to the sender. Ensure that your Bot accepts HL7 inputs, and you may refer to the [HL7 Bots documentation](/docs/bots/hl7-into-fhir) for more advanced uses. For an initial setup, the minimal Bot may be used: ```js import { BotEvent, Hl7Message, MedplumClient } from '@medplum/core'; export async function handler(medplum: MedplumClient, event: BotEvent): Promise { return event.input.buildAck(); } ``` ##### Set up an Agent Resource Third, set up the [Agent](https://app.medplum.com/Agent/new). This identifies the Agent in the field and helps route and manage the traffic from that agent appropriately. Give the `Agent` a descriptive name. You'll want to link the Agent to the Endpoint and Bot you created to support its setup. ```json { "resourceType": "Agent", "name": "ACME Hospital Admissions", "status": "active", "channel": [ { "name": "ADT Channel", "endpoint": { "reference": "Endpoint/my-endpoint-uuid", "display": "Test Endpoint" }, "targetReference": { "reference": "Bot/my-bot-uuid", "display": "HL7 ADT Processing Bot" } } ] } ``` ##### Set up a Client Application Finally, set up your [Client Application](https://app.medplum.com/admin/clients/new). While a default client may be used, we recommend setting up a new client for traceability. We also recommend setting up an AccessPolicy and attaching it to the ClientApplication you can restrict access to the host. The instructions on [setting up access controls](/docs/user-management#creating-memberships) are available in the Admin panel. At the end of the setup you should have the following items: - Base Url - The base URL of your Medplum server, or https://api.medplum.com for hosted Medplum - `ClientId`/`ClientSecret` - obtain these from the [ClientApplication](https://app.medplum.com/ClientApplication/) you created or will use in the Medplum app. - Agent ID - this is the Agent `id` UUID You'll need the above to successfully install and connect the agent. #### Install on Host This guide walks through how to install the agent onto the host. The agent connects to Medplum using [device authentication](/docs/auth). ##### Running from Source via CLI The agent can be run locally using the agent package directory in the Medplum repo. Keep the terminal open after setup so that you can see the logs. You can get started by following these steps: 1. Clone the [Medplum repo](https://github.com/medplum/medplum). 2. Run the following commands from the top level of the repo. ```sh npm ci cd packages/agent npm run agent ``` ##### Windows Install The agent executable for Windows is built with each release, and can be be downloaded from the [Medplum Releases](https://github.com/medplum/medplum/releases) page. To install on a Windows Host, remote into the host and download the agent executable to the host filesystem. Double click on the installer executable to start and go through the install screen, inputting the 4 pieces of information from the previous step into the screen. Once the install is complete you should be able to see the following: - [ ] The agent running as a Windows Service named "Medplum Agent" - [ ] Check the Medplum Agent logs typically found at `C:\Program Files\Medplum Agent` in a standard install ##### Linux Install The Linux version of the Medplum Agent is currently available as a standalone binary. You can get started by following these steps: 1. Download the latest Linux binary from the [Medplum Releases](https://github.com/medplum/medplum/releases/latest) page. 2. Make the binary executable ```bash chmod +x medplum-agent-linux ``` 3. Run the agent with your configuration parameters ```bash ./medplum-agent-linux ``` ##### Docker Image For instructions on building and running the Docker image for the Medplum Agent, please refer to the [Medplum Agent package documentation](https://github.com/medplum/medplum/blob/main/packages/agent). ### Testing your Setup Go through the following checklist to ensure that your setup is working. #### Testing a site install - [ ] When you log into the host, ensure that the agent is running - [ ] Send a message to the Agent for testing - [ ] Confirm that the logs on the agent host - [ ] Navigate to your [`Bot` on the Medplum App](https://app.medplum.com/Bot) and view the `Events` tab, confirm that you see the AuditEvents produced #### Testing HL7 with Medplum CLI For a simple end-to-end test of HL7 over MLLP, we recommend using [Medplum CLI](/docs/cli) as follows. Install the Medplum CLI if you have not done so already: ```bash npm install --global @medplum/cli ``` See [Medplum CLI](/docs/cli) for more detailed installation instructions. Then send a test message: ```bash medplum hl7 send localhost 56000 --generate-example ``` You can also send a specific test message, stored in a `.hl7`, similar extension (`.oru`, `.adt`, etc.), or `.txt.` file, via the following command: ```bash npx medplum hl7 send localhost 56000 --file ``` There should now be an `AuditEvent` in Medplum for the message. #### Testing HL7 with Sim Hospital For a more comprehensive end-to-end test of HL7 over MLLP, we recommend using [Sim Hospital](https://github.com/google/simhospital) as follows. (Assumes you have docker installed) ```bash docker run --rm -it -p 8000:8000 eu.gcr.io/simhospital-images/simhospital:latest health/simulator -output mllp -mllp_destination : -pathways_per_hour 720 ``` If you are testing your local setup on Linux/Mac with the agent running in docker you will need to do the following: ```bash docker run --rm -it -p 8000:8000 eu.gcr.io/simhospital-images/simhospital:latest health/simulator -output mllp -mllp_destination host.docker.internal: -pathways_per_hour 720 ``` ### Audit Events and Logging All interactions between a `Bot` and an `Agent`, such as calling `Agent` operations from a `Bot` or calling `Agent` operations that `$execute` a `Bot`, will trigger either the creation of `AuditEvent` resources or the configured logging behavior for the `Bot` (see: [Bot logging configuration](/docs/bots/bots-in-production#configuring-bot-logging)). :::note[] Because a reference to the `Agent` that interacted with the `Bot` is logged in the `AuditEvent` created by the `Bot`, these events will show up in the `Events` tab for both the `Bot` and the `Agent`. These are not duplicated `AuditEvent` resources but rather the same resource showing up in both `Event` log views. ::: ### Preparing for Scale HL7 Feeds can be extremely high volume, and before you go live with a high-volume feed you may want to modify your Bot to log only set the Bot `AuditEventDestination` to `log`. ### Supported Protocols - [HL7 v2](https://www.hl7.org): A widely used low-level message protocol for healthcare data exchange. - [DICOM](https://www.dicomstandard.org/): A protocol used for storing medical images and their metadata. Currently, Medplum supports the C-STORE and C-ECHO operations. Support for other DICOM operations such C-FIND, C-GET, and C-MOVE is coming soon. See [DICOM with the Medplum Agent](/docs/dicom/agent-dimse) for channel setup, and [DICOM & DICOMweb](/docs/dicom) for how received studies are stored and served. - [ASTM](https://www.astm.org/): Used to transfer data between clinical instruments and computer systems. ASTM support is still in [alpha](/docs/compliance/alpha-beta). ### See also - [Medplum Agent](/solutions/agent) - [Medplum Agent design discussion](https://github.com/medplum/medplum/discussions/2012) - [Medplum WebSockets design discussion](https://github.com/medplum/medplum/discussions/1490) - [FHIR WebSocket Subscriptions](https://build.fhir.org/ig/HL7/fhir-subscription-backport-ig/channels.html#websockets) - [FHIRcast](https://fhircast.org/) - [Medplum CLI](/docs/cli) - [Sim Hospital](https://github.com/google/simhospital) is a useful tool to generate HL7 V2 test messages locally for testing - [Medplum Agent Demo Video](https://youtu.be/MmE3Dn939B4) on Youtube --- ## Manual Uninstall The official agent installer for Microsoft Windows includes an uninstaller. However, if you need to manually uninstall the agent, follow these steps. ### Delete the Agent Service As an administrator, open a command prompt and run: ```bash sc.exe stop MedplumAgent sc.exe delete MedplumAgent ``` ### Delete the Agent Files :::info[] Make sure the agent is stopped and deleted before attempting to delete the files. ::: :::warning[] This will delete all configuration files and log data. ::: As an administrator, delete all files in the following directories: 1. `C:\Program Files\MedplumAgent` 2. `C:\Users\$USERNAME\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Medplum Agent` ### Delete the Agent Registry Keys As an administrator, open the registry editor and delete the following keys (if they exist): 1. `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MedplumAgent` 2. `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MedplumAgent` --- ## Push to Agent ### Introduction While the Medplum Agent traditionally connected healthcare facilities to cloud services by listening to legacy healthcare protocols and forwarding messages securely to the cloud, it also supports pushing messages from the cloud to on-prem devices. ### Feature Overview 1. **Message Push**: Leverage websockets for a persistent connection between the cloud server and on-prem Medplum Agent. 2. **Cloud-Initiated Messages**: With the existing connection, the server can initiate messages to be sent directly to remote devices via the agent. 3. **Use Cases**: - Push order details directly to lab instruments using HL7 or ASTM. - Send patient details to radiology modalities using DICOM. - Relay a DICOM SR to a RIS using DICOM. 4. **Simplified Infrastructure**: Eliminate the need for site-to-site VPNs, making setups more straightforward and secure. ### How it works First, consider the most simple case: an HL7 device on the network (e.g. a Radiology Information System or Lab Instrument): ![Medplum Agent Push 1](./medplum-agent-push-1.webp) We can emulate this setup using the Medplum CLI: ```bash medplum hl7 listen 56000 ``` Next, let's add a simple HL7 client also on the network: ![Medplum Agent Push 2](./medplum-agent-push-2.webp) We can emulate this step using the Medplum CLI: ```bash medplum hl7 send localhost 56000 --generate-example ``` This will send a message and print the reply to the terminal. The main challenge with HL7 interfaces is the need to be on the local network. Historically, this meant running the EHR software on-premise. Instead, we will use the Medplum Agent to proxy traffic to the Medplum server hosted in the cloud: ![Medplum Agent Push 3](./medplum-agent-push-3.webp) The Medplum Agent connects to the on-premise device the same as any other HL7 client: using HL7 over MLLP. The power of the Medplum Agent is when connected to a Medplum server: ![Medplum Agent Push 4](./medplum-agent-push-4.webp) The Agent connects to the Server using secure WebSockets (WSS). Because WebSockets are a persistent connection, the server can "push" a message to the instrument via the agent. ![Medplum Agent Push 5](./medplum-agent-push-5.webp) We can emulate this step using the Medplum CLI. This time, instead of using HL7 over MLLP, we will send a JSON message over HTTPS. The Server forwards the message to the Agent. The Agent forwards the message to the instrument. ```bash medplum post 'Agent/1080ee82-e4fc-4312-946f-322fbecf9bb9/$push' $'{"destination":"Device/52c9a9de-1081-4fb2-a40a-e62efe1b73b2","contentType":"x-application/hl7-v2+er7","body":"MSH|^~\\\\&|ADTSYS|HOSPITAL|RECEIVER|DEST|20231023235305.516||ADT^A01|1698105185516|P|2.5|\\nEVN|A01|20231023235305.516||\\nPID|1|12345|12345^^^HOSP^MR|123456|DOE^JOHN^MIDDLE^SUFFIX|19800101|M|||123 STREET^APT 4B^CITY^ST^12345-6789||555-555-5555||S|\\nPV1|1|I|2000^2012^01||||12345^DOCTOR^DOC||||||||||1234567^DOCTOR^DOC||AMB|||||||||||||||||||||||||202309280900|"}' ``` This line is intentionally long due to the complexities of escaping new lines. Let's break it down piece by piece: ```bash medplum post 'Agent/1080ee82-e4fc-4312-946f-322fbecf9bb9/$push' $'{ "destination": "Device/52c9a9de-1081-4fb2-a40a-e62efe1b73b2", "contentType": "x-application/hl7-v2+er7", "body": "MSH|^~\\\\&|ADTSYS|HOSPITAL|RECEIVER|DEST|20231023235305.516||ADT^A01|1698105185516|P|2.5|\\n\ EVN|A01|20231023235305.516||\\n\ PID|1|12345|12345^^^HOSP^MR|123456|DOE^JOHN^MIDDLE^SUFFIX|19800101|M|||123 STREET^APT 4B^CITY^ST^12345-6789||555-555-5555||S|\\n\ PV1|1|I|2000^2012^01||||12345^DOCTOR^DOC||||||||||1234567^DOCTOR^DOC||AMB|||||||||||||||||||||||||202309280900|"\ }' ``` ### How To Use The Feature 1. **Direct Push**: - Send an HTTPS POST request to `/Agent/{id}/$push`. - The content body should be a JSON object with the following properties: - `destination` - Device reference (i.e., `Device/123`) or a Device search (i.e., `Device?identifier=123`) - The `Device` resource must have a `Device.url` property - `contentType` - MIME type of the content, `x-application/hl7-v2+er7` for HL7 v2 - `body` - the actual HL7 message body - The Medplum server will forward the message through websockets, and the Medplum Agent will relay it to the intended device. 2. **Using MedplumClient**: - MedplumClient, Medplum's JavaScript/TypeScript SDK, simplifies tasks related to connectivity and authentication. - To send a message, just use: `medplum.pushToAgent(agent, device, body, contentType)`. The SDK manages the HTTPS POST request for you. 3. **Pushing with Medplum Bots**: - Medplum Bots allow users to run custom JavaScript or TypeScript logic based on various triggers. - Each Bot invocation has a built-in MedplumClient. - To push a message, use: `medplum.pushToAgent(agent, device, body, contentType)`. ### End-To-End Example: Pushing Lab Orders **Objective**: Push lab orders to a remote device using HL7. **Trigger**: "Create" or "Update" operations on a FHIR ServiceRequest. **Steps**: 1. Set up a Medplum Bot. - The Bot will receive the FHIR ServiceRequest. - It will then convert the required FHIR fields to HL7 using the Medplum HL7 SDK. 2. Save and deploy your Bot. 3. Create a FHIR Subscription: - Set the criteria to "ServiceRequest". - Choose the Bot as the channel target. 4. Now, any "create" or "update" operation on a FHIR ServiceRequest will trigger the Medplum server to call the Bot. The Bot will process and convert the message to HL7, sending it to the remote device via the Medplum Agent. --- ## Agent Reload Config Remotely reloads the configuration of a given agent or agents from the Medplum Server. Useful for pushing changes made to an `Agent` resource (such as adding, removing, or modifying channels) to a running agent without restarting the agent service. :::info[Minimum versions] The `$reload-config` operation requires **Medplum Server > 3.1.6** and **Medplum Agent > 3.1.6**. See the [feature matrix](./features.md) for more details. ::: ### Invoke the `$reload-config` operation #### Single Agent ``` [base]/Agent/[id]/$reload-config ``` For example: ```bash medplum get 'Agent/[id]/$reload-config' ``` #### Multiple Agents ``` [base]/Agent/$reload-config ``` For example: ```bash medplum get 'Agent/$reload-config?status=active' ``` Here, `status=active` is an `Agent` search parameter used to select which agents to reload. See [Using Agent search parameters in bulk operations](./using-search-parameters.md) for more ways to select operation targets. ### Single Agent Response When invoking the operation on a single agent by ID, the response is the result of the reload request for that agent, either an `OperationOutcome` or `Parameters` resource. #### Valid Response Example response when the agent accepts the reload request: ```json { "resourceType": "OperationOutcome", "id": "ok", "issue": [ { "severity": "information", "code": "informational", "details": { "text": "All OK" } } ] } ``` ### Multi-Agent Response When invoking the operation without an ID (querying multiple agents via search parameters), the response is a `Bundle` of `Parameters`. Each `Parameters` within the `Bundle` contains an `agent` and a `result`, which is the result of calling the `$reload-config` operation on that `Agent`, either a `Parameters` or `OperationOutcome` resource. #### Valid Response Example response for multiple agents: ```json { "resourceType": "Bundle", "type": "collection", "entry": [ { "resource": { "resourceType": "Parameters", "parameter": [ { "name": "agent", "resource": { "resourceType": "Agent", "name": "Test Agent 1", "status": "active", "id": "93f8b2fb-65a3-4977-a175-71b73b26fde7", "meta": { "versionId": "e182201a-6925-467f-a92b-496193fb4c39", "lastUpdated": "2024-04-19T20:29:25.087Z" } } }, { "name": "result", "resource": { "resourceType": "OperationOutcome", "id": "ok", "issue": [ { "severity": "information", "code": "informational", "details": { "text": "All OK" } } ] } } ] } }, { "resource": { "resourceType": "Parameters", "parameter": [ { "name": "agent", "resource": { "resourceType": "Agent", "name": "Test Agent 2", "status": "active", "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890", "meta": { "versionId": "f293201a-6925-467f-a92b-496193fb4c39", "lastUpdated": "2024-04-19T20:29:25.087Z" } } }, { "name": "result", "resource": { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "exception", "details": { "text": "Timeout" } } ] } } ] } } ] } ``` #### Invalid Response Example outcome when no agents match the given query: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "No agent(s) for given query" } } ] } ``` Example outcome when exceeding max `_count` limit: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "'_count' of 101 is greater than max of 100" } } ] } ``` ### Using search parameters All of the `Agent` search parameters can be used to select which agents to reload when using the multi-agent endpoint. Some useful search parameters are: - `name` - `status` - `_count` and `_offset` :::note[Default page size] When `_count` is omitted, the operation reloads at most the **default page of 20 agents** — it does _not_ automatically run against every matching agent. The maximum allowed `_count` is `100`. To reload more agents than fit on one page, use `_count` and `_offset` to page through the results (see [Paging Through Agents](#paging-through-agents)). ::: > For more recipes and details on selecting operation targets, see [Using Agent search parameters in bulk operations](./using-search-parameters.md). ### Recipes #### Single Agent by ID Reload the config for a specific agent: ```bash medplum get 'Agent/93f8b2fb-65a3-4977-a175-71b73b26fde7/$reload-config' ``` #### Single Agent by Identifier Reload the config for a specific agent by identifier: ```bash medplum get 'Agent/$reload-config?identifier=agent-007' ``` #### Multiple Agents by Name Reload agents with a specific name prefix (without a `_count`, this acts on at most the default page of 20 agents): ```bash medplum get 'Agent/$reload-config?name=Production+Agent' ``` #### Multiple Agents by Status Reload active agents (without a `_count`, this acts on at most the default page of 20 agents): ```bash medplum get 'Agent/$reload-config?status=active' ``` #### Paging Through Agents Reload agents in batches, 50 agents at a time: ```bash medplum get 'Agent/$reload-config?_count=50&_offset=0' medplum get 'Agent/$reload-config?_count=50&_offset=50' ``` :::tip[CLI] You can also invoke this operation using the Medplum CLI's [`agent reload-config` command](./agent-cli-commands.md#reload-config-command). ::: --- ## System Requirements Medplum publishes an official agent installer for Microsoft Windows. To install on a Windows Host, remote into the host and download the agent executable to the host filesystem. Double click on the MSI to start and go through the install screen, inputting the 4 pieces of information from the previous step into the screen. The agent executable for Windows is built with each release, and can be be downloaded from the [releases](https://github.com/medplum/medplum/releases) page. #### Operating System (OS) - **Minimum OS**: Microsoft Windows 8.1 or Windows Server 2012 R2 - **Recommended OS**: Microsoft Windows 11 or Windows Server 2022 #### Random Access Memory (RAM) - **Minimum RAM**: 512 MB - Adequate for small-scale applications with minimal processing demands. - **Recommended RAM**: 2 GB or more - Advisable for applications that manage extensive in-memory operations, exhibit high levels of user concurrency, or execute multiple background processes. #### Central Processing Unit (CPU) - **Minimum CPU**: 1 GHz or faster processor, 64-bit (x64) architecture. Single-core CPUs are capable of running Node.js; however, performance may be limited under intensive workloads. - **Recommended CPU**: Multi-core processor (2 cores or more). Node.js inherently operates on a single thread, but it can leverage multi-core systems using the cluster module, facilitating concurrent processing and enhancing throughput. #### Disk Storage - **Minimum Disk Space**: 100-200 MB - Sufficient for the Node.js runtime environment and a basic application footprint. - **Recommended Disk Space**: 1 GB or greater - Recommended to accommodate application dependencies, log files, user data, and temporary files. Solid State Drives (SSDs) are preferable for improved read/write performance, which is particularly beneficial when the application frequently accesses the disk. --- **Note**: These specifications represent a baseline that should ensure the functional operation of a standard Node.js application. Actual requirements may vary based on specific application characteristics and workload conditions. We recommend conducting performance evaluations to fine-tune these recommendations to match the application's production environment and anticipated usage patterns. --- ## Agent Stats Fetches runtime statistics from a given agent or agents. Useful for monitoring connection counts, queue depths, RTT metrics, and overall agent health. ### Invoke the `$stats` operation #### Single Agent ``` [base]/Agent/[id]/$stats ``` For example: ```bash medplum get 'Agent/[id]/$stats' ``` #### Multiple Agents ``` [base]/Agent/$stats ``` For example: ```bash medplum get 'Agent/$stats?_tag=Group+A' ``` Here, `_tag=Group+A` is an `Agent` search parameter used to select which agents to fetch stats from. For example, to fetch stats from all active agents use `status=active`. See [Using Agent search parameters in bulk operations](./using-search-parameters.md) for more ways to select operation targets. ### Single Agent Response When querying a single agent by ID, the response is a `Parameters` resource containing the stats payload. #### Valid Response Example response when stats are successfully retrieved: ```json { "resourceType": "Parameters", "parameter": [ { "name": "stats", "valueString": "{\"hl7ConnectionsOpen\":1,\"ping\":5,\"webSocketQueueDepth\":0,\"hl7QueueDepth\":0,\"hl7ClientCount\":0,\"live\":true,\"outstandingHeartbeats\":0,\"channelStats\":{},\"clientStats\":{}}" } ] } ``` The `stats` parameter contains a JSON-encoded object describing the agent's runtime state. Known fields include: - `hl7ConnectionsOpen`: Number of currently open inbound HL7 connections - `ping`: Most recent measured ping (round-trip time) to the Medplum server, in milliseconds - `webSocketQueueDepth`: Number of messages currently queued for delivery over the WebSocket - `hl7QueueDepth`: Number of HL7 messages currently queued for processing - `hl7ClientCount`: Number of outbound HL7 client connections currently held in the pool - `live`: Whether the agent considers itself live and connected to the server - `outstandingHeartbeats`: Number of outstanding (unacknowledged) heartbeats sent to the server - `channelStats`: Per-channel statistics, including round-trip-time metrics for inbound channels - `clientStats`: Per-remote statistics for outbound HL7 clients, including round-trip-time metrics Additional fields may be present and should be treated as informational. ### Multi-Agent Response When querying multiple agents, the response is a `Bundle` of `Parameters`. Each `Parameters` within the `Bundle` contains an `agent` and a `result`, which is the result of calling the `$stats` operation on this `Agent`, either a `Parameters` or `OperationOutcome` resource. #### Valid Response Example response for multiple agents: ```json { "resourceType": "Bundle", "type": "collection", "entry": [ { "resource": { "resourceType": "Parameters", "parameter": [ { "name": "agent", "resource": { "resourceType": "Agent", "name": "Test Agent 1", "status": "active", "id": "93f8b2fb-65a3-4977-a175-71b73b26fde7", "meta": { "versionId": "e182201a-6925-467f-a92b-496193fb4c39", "lastUpdated": "2024-04-19T20:29:25.087Z" } } }, { "name": "result", "resource": { "resourceType": "Parameters", "parameter": [ { "name": "stats", "valueString": "{\"hl7ConnectionsOpen\":1,\"ping\":5,\"webSocketQueueDepth\":0,\"hl7QueueDepth\":0,\"hl7ClientCount\":0,\"live\":true,\"outstandingHeartbeats\":0,\"channelStats\":{},\"clientStats\":{}}" } ] } } ] } }, { "resource": { "resourceType": "Parameters", "parameter": [ { "name": "agent", "resource": { "resourceType": "Agent", "name": "Test Agent 2", "status": "active", "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890", "meta": { "versionId": "f293201a-6925-467f-a92b-496193fb4c39", "lastUpdated": "2024-04-19T20:29:25.087Z" } } }, { "name": "result", "resource": { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "exception", "details": { "text": "Failed to fetch stats: Agent not responding" } } ] } } ] } } ] } ``` #### Invalid Response Example outcome when an ID was not supplied to the operation: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "Must specify agent ID or identifier" } } ] } ``` ### Using search parameters All of the `Agent` search parameters can be used to select which agents to query stats from when using the multi-agent endpoint. Some useful search parameters are: - `name` - `status` - `_count` and `_offset` :::note[Default page size] When `_count` is omitted, the operation fetches stats from at most the **default page of 20 agents** — it does _not_ automatically run against every matching agent. The maximum allowed `_count` is `100`. To cover more agents than fit on one page, use `_count` and `_offset` to page through the results (see [Paging Through Agent Stats](#paging-through-agent-stats)). ::: > For more recipes and details on selecting operation targets, see [Using Agent search parameters in bulk operations](./using-search-parameters.md). ### Recipes #### Single Agent by ID Fetch stats from a specific agent by ID: ```bash medplum get 'Agent/93f8b2fb-65a3-4977-a175-71b73b26fde7/$stats' ``` #### Single Agent by Identifier Fetch stats from a specific agent by identifier: ```bash medplum get 'Agent/$stats?identifier=agent-007' ``` #### Multiple Agents by Name Fetch stats from agents with a specific name prefix (without a `_count`, this acts on at most the default page of 20 agents): ```bash medplum get 'Agent/$stats?name=Production+Agent' ``` #### Multiple Agents by Status Fetch stats from active agents (without a `_count`, this acts on at most the default page of 20 agents): ```bash medplum get 'Agent/$stats?status=active' ``` #### Paging Through Agent Stats Fetch stats from agents in batches, 50 agents at a time: ```bash medplum get 'Agent/$stats?_count=50&_offset=0' medplum get 'Agent/$stats?_count=50&_offset=50' ``` --- ## Agent Status Gets the status of a given agent. Useful for seeing whether an agent is connected and listing its current software version. > For querying multiple agent statuses at once, or using `SearchParameters` to select agents to query, see [Bulk Status](./bulk-status). ### Invoke the `$status` operation ``` [base]/Agent/[id]/$status ``` For example: ```bash medplum get 'Agent/[id]/$status' ``` #### Valid Response Valid status codes include: - `connected` - `disconnected` - `unknown` Example response when the `Agent` is known and connected: ```json { "resourceType": "Parameters", "parameter": [ { "name": "status", "valueCode": "connected" }, { "name": "version", "valueString": "3.1.4" }, { "name": "lastUpdated", "valueInstant": "2024-04-19T00:00:00Z" } ] } ``` In cases where status has not been reported yet, `status` and `version` may be `unknown`, and `lastUpdated` may not be present. ```json { "resourceType": "Parameters", "parameter": [ { "name": "status", "valueCode": "unknown" }, { "name": "version", "valueString": "unknown" } ] } ``` #### Invalid Response Example outcome when an ID was not supplied to the operation: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "Must specify agent ID or identifier" } } ] } ``` --- ## Troubleshooting ### Issue - Agent Logs display `Token Expired` and service will not run. The log will state `[DEBUG] stderr: "Error: Token expired"` - This error relates to the Authentication Token timing of when the Token was issued and when it is being used. It is best to check the system clock on the system where the Agent is installed. Ensure that the clock skew is no greater than 1 minute. Adjusting the system time and trying to restart, a new token will be issued. ### Log Level To adjust the Agent log level, the `agent.properties` located within `C:\Program Files\Medplum Agent`, needs changed to reflect the desired level. The default logLevel is `INFO`, to increase the logging out add the `logLevel` key with the level as below ``` logLevel=DEBUG ``` Once the `agent.properties` is changed, the Windows Service `Medplum Agent` must be restarted for the changes take effect. The log will show an output denoting the set level - that is embedded within the JSON structure after the `level` property. ``` 2024-10-29 16:52:56 [DEBUG] stdout: "{\"level\":\"DEBUG\", ... ``` To set the logLevel back to the default, remove the `logLevel` key from the `agent.properties` file and restart the Windows Service. For more detailed configuration options, see the [Configuration](./configuration) page. --- ## Agent Upgrade Remotely upgrades the Medplum Agent software for a given agent or agents. Useful for rolling out new agent versions without needing physical or remote access to the host machine running the agent. :::info[Minimum versions] The `$upgrade` operation requires **Medplum Server > 3.1.9** and **Medplum Agent > 3.1.10**. See the [feature matrix](./features.md) for more details. ::: ### Invoke the `$upgrade` operation #### Single Agent ``` [base]/Agent/[id]/$upgrade ``` For example: ```bash medplum get 'Agent/[id]/$upgrade' ``` #### Multiple Agents ``` [base]/Agent/$upgrade ``` For example: ```bash medplum get 'Agent/$upgrade?status=active' ``` Here, `status=active` is an `Agent` search parameter used to select which agents to upgrade. See [Using Agent search parameters in bulk operations](./using-search-parameters.md) for more ways to select operation targets. ### Parameters - `version` (optional; default: latest published version): The version to upgrade the agent(s) to. When omitted, the agent upgrades to the latest available version. - `timeout` (optional; default: `45000`, max: `56000`): How long, in milliseconds, the server waits for the agent to acknowledge the upgrade request before returning an error. Values above the maximum are clamped to `56000`. - `force` (optional; default: `false`): When `true`, instructs the agent to upgrade even if it is already running the requested version. > The parameters above configure _how_ the upgrade runs. To select _which_ agents to upgrade when using the multi-agent endpoint, use `Agent` search parameters — see [Using Agent search parameters in bulk operations](./using-search-parameters.md). ### Single Agent Response When invoking the operation on a single agent by ID, the response is the result of the upgrade request for that agent, either an `OperationOutcome` or `Parameters` resource. #### Valid Response Example response when the agent accepts the upgrade request: ```json { "resourceType": "OperationOutcome", "id": "ok", "issue": [ { "severity": "information", "code": "informational", "details": { "text": "All OK" } } ] } ``` ### Multi-Agent Response When invoking the operation without an ID (querying multiple agents via search parameters), the response is a `Bundle` of `Parameters`. Each `Parameters` within the `Bundle` contains an `agent` and a `result`, which is the result of calling the `$upgrade` operation on that `Agent`, either a `Parameters` or `OperationOutcome` resource. #### Valid Response Example response for multiple agents: ```json { "resourceType": "Bundle", "type": "collection", "entry": [ { "resource": { "resourceType": "Parameters", "parameter": [ { "name": "agent", "resource": { "resourceType": "Agent", "name": "Test Agent 1", "status": "active", "id": "93f8b2fb-65a3-4977-a175-71b73b26fde7", "meta": { "versionId": "e182201a-6925-467f-a92b-496193fb4c39", "lastUpdated": "2024-04-19T20:29:25.087Z" } } }, { "name": "result", "resource": { "resourceType": "OperationOutcome", "id": "ok", "issue": [ { "severity": "information", "code": "informational", "details": { "text": "All OK" } } ] } } ] } }, { "resource": { "resourceType": "Parameters", "parameter": [ { "name": "agent", "resource": { "resourceType": "Agent", "name": "Test Agent 2", "status": "active", "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890", "meta": { "versionId": "f293201a-6925-467f-a92b-496193fb4c39", "lastUpdated": "2024-04-19T20:29:25.087Z" } } }, { "name": "result", "resource": { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "exception", "details": { "text": "Timeout" } } ] } } ] } } ] } ``` #### Invalid Response Example outcome when no agents match the given query: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "No agent(s) for given query" } } ] } ``` Example outcome when exceeding max `_count` limit: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "'_count' of 101 is greater than max of 100" } } ] } ``` ### Using search parameters All of the `Agent` search parameters can be used to select which agents to upgrade when using the multi-agent endpoint. Some useful search parameters are: - `name` - `status` - `_count` and `_offset` :::note[Default page size] When `_count` is omitted, the operation upgrades at most the **default page of 20 agents** — it does _not_ automatically run against every matching agent. The maximum allowed `_count` is `100`. To upgrade more agents than fit on one page, use `_count` and `_offset` to page through the results (see [Paging Through Agents](#paging-through-agents)). ::: ### Recipes #### Single Agent by ID Upgrade a specific agent to the latest version: ```bash medplum get 'Agent/93f8b2fb-65a3-4977-a175-71b73b26fde7/$upgrade' ``` #### Single Agent to a Specific Version Upgrade a specific agent to a pinned version: ```bash medplum get 'Agent/93f8b2fb-65a3-4977-a175-71b73b26fde7/$upgrade?version=3.1.10' ``` #### Single Agent by Identifier Upgrade a specific agent by identifier: ```bash medplum get 'Agent/$upgrade?identifier=agent-007' ``` #### Multiple Agents by Name Upgrade agents with a specific name prefix (without a `_count`, this acts on at most the default page of 20 agents): ```bash medplum get 'Agent/$upgrade?name=Production+Agent' ``` #### Multiple Agents by Status Upgrade active agents (without a `_count`, this acts on at most the default page of 20 agents): ```bash medplum get 'Agent/$upgrade?status=active' ``` #### Force an Upgrade Upgrade an agent even if it is already running the requested version: ```bash medplum get 'Agent/93f8b2fb-65a3-4977-a175-71b73b26fde7/$upgrade?version=3.1.10&force=true' ``` #### Setting a Custom Timeout Wait up to 30 seconds for the agent to acknowledge the upgrade request: ```bash medplum get 'Agent/93f8b2fb-65a3-4977-a175-71b73b26fde7/$upgrade?timeout=30000' ``` #### Paging Through Agents Upgrade agents in batches, 50 agents at a time: ```bash medplum get 'Agent/$upgrade?_count=50&_offset=0' medplum get 'Agent/$upgrade?_count=50&_offset=50' ``` :::tip[CLI] You can also invoke this operation using the Medplum CLI's [`agent upgrade` command](./agent-cli-commands.md#upgrade-command). ::: --- ## Using Agent Search Parameters in Bulk Operations Several of the Medplum Agent operations can act on more than one agent at a time. Instead of targeting a single agent by ID, you invoke the operation against the `Agent` _type_ endpoint and use `Agent` search parameters to select which agents the operation should run against. This page collects common "recipes" for selecting agents with search parameters, so you don't have to look up the same query patterns on each individual operation page. ### Operations that support search parameters The following operations accept `Agent` search parameters to select their operation targets when invoked against the type-level endpoint (`[base]/Agent/$operation`): - [`$bulk-status`](./bulk-status.md) - [`$fetch-logs`](./fetch-logs.md) - [`$stats`](./stats.md) - [`$upgrade`](./upgrade.md) In each case, the operation is run against _every_ agent matched by the query, and the response is a `Bundle` of `Parameters`, one entry per matched agent. :::info[Single vs. multiple agents] The `$status` operation is single-agent only and does **not** accept search parameters. To query the status of multiple agents at once, use [`$bulk-status`](./bulk-status.md). ::: ### Available `Agent` search parameters The `Agent` resource defines the following resource-specific search parameters: - `name` — the agent's name (string search; matches by prefix) - `identifier` — an `Agent.identifier` (token search) - `status` — the agent's status, e.g. `active` or `off` (token search) In addition, the standard "global" search parameters apply, including: - `_id` — the agent's logical ID - `_tag` — a tag on `Agent.meta.tag` - `_lastUpdated` — when the agent resource was last updated - `_count` and `_offset` — for paging through large result sets :::note[Default and maximum page size] When `_count` is omitted, these operations act on at most the **default page size of 20 agents** — they do _not_ automatically run against every matching agent. The maximum allowed `_count` is `100`; requesting a larger `_count` returns an `OperationOutcome` error. To operate on more agents than fit on a single page, use `_count` and `_offset` to page through the results — see [Paging through agents](#paging-through-agents) below. ::: ### Recipes The recipes below use [`$bulk-status`](./bulk-status.md) as an example, but the same query patterns work for any operation that supports search parameters — just swap in the operation you want to run (`$fetch-logs`, `$stats`, `$upgrade`, etc.). #### All active agents Select agents whose status is `active` — the most common multi-agent query. Note that without a `_count`, this acts on at most the default page of 20 agents (see [Paging through agents](#paging-through-agents) to cover more): ```bash medplum get 'Agent/$bulk-status?status=active' ``` #### Agents matching a name prefix Select all agents whose name starts with a given prefix: ```bash medplum get 'Agent/$bulk-status?name=Production+Agent' ``` #### Agents with a given tag Select all agents tagged with a particular value (useful for grouping agents by site, region, or deployment group): ```bash medplum get 'Agent/$bulk-status?_tag=Group+A' ``` #### Combining search parameters Search parameters can be combined to narrow the selection. For example, all `active` agents in `Group A`: ```bash medplum get 'Agent/$bulk-status?status=active&_tag=Group+A' ``` #### Paging through agents When operating on a large number of agents, page through them in batches using `_count` and `_offset`: ```bash medplum get 'Agent/$bulk-status?_count=50&_offset=0' medplum get 'Agent/$bulk-status?_count=50&_offset=50' ``` ### Targeting a single agent by name or identifier In addition to selecting an agent by its logical ID (`[base]/Agent/[id]/$operation`), you can target a single agent against the type-level endpoint by using a search parameter that you expect to be unique, most commonly `name` or `identifier`: ```bash medplum get 'Agent/$bulk-status?name=Test+Agent+1' ``` ```bash medplum get 'Agent/$fetch-logs?identifier=agent-007' ``` :::caution[These are conventions, not guarantees] Using `name` or `identifier` to target a "single" agent is only a convention. Neither `name` nor `identifier` is required to be unique across agents in a project — if the same name or identifier is shared by multiple agents, the operation will match and run against **all** of them, and the response will be a `Bundle` with an entry for each matched agent rather than a single result. If you need to operate on exactly one specific agent, target it by its logical ID instead: ```bash medplum get 'Agent/93f8b2fb-65a3-4977-a175-71b73b26fde7/$bulk-status' ``` ::: :::tip[CLI] Many of these operations can also be invoked using the Medplum CLI. See the [agent CLI commands](./agent-cli-commands.md) for details. ::: --- ## Medplum `$ai` Operation ## Medplum `$ai` Operation Medplum's `$ai` operation enables integration with large language models (LLMs) through a FHIR-compliant interface. This operation allows you to leverage AI capabilities for conversational interactions and FHIR function calling within your healthcare applications. ### Overview The AI operation provides: - **Direct OpenAI Integration**: Call OpenAI models (GPT-4, GPT-3.5-turbo, etc.) through Medplum's FHIR API - **FHIR Function Calling**: Enable AI models to suggest FHIR operations (search, create, update, delete resources) - **Streaming Support**: Get real-time streaming responses for conversational interfaces - **Conversation History**: Maintain multi-turn conversations with full message history ### Prerequisites Before using the `$ai` operation, you need: 1. **Enable AI Feature**: The `ai` feature must be enabled on your Medplum project 2. **Configure OpenAI API Key**: Add your OpenAI API key as a project secret named `OPENAI_API_KEY` in your project settings 3. **Use a Supported Account**: `$ai` is intended for practitioners, bots and client applications. For patient-facing AI features, call `$ai` from a Bot #### Using a LiteLLM Proxy (or other OpenAI-compatible endpoint) By default, the `$ai` operation calls OpenAI directly at `https://api.openai.com/v1`. To route requests through a [LiteLLM](https://docs.litellm.ai/) proxy — or any other OpenAI-compatible API — add an optional project secret named `LLM_BASE_URL` set to the base URL of the proxy (for example `https://litellm.example.com/v1`). The operation appends `/chat/completions` to this value. When `LLM_BASE_URL` is not set, the operation calls OpenAI as before. The `OPENAI_API_KEY` secret is sent as the `Authorization: Bearer` token to whichever endpoint is configured, so set it to the LiteLLM proxy's key (master key or virtual key) when using a proxy. Because LiteLLM is OpenAI-API-compatible, the request and response formats — including function calling and streaming — are unchanged, and the `model` parameter accepts any model name your proxy is configured to serve. ### Endpoint ``` POST [baseUrl]/fhir/R4/$ai ``` ### Parameters #### Input Parameters | Name | Type | Required | Description | | ------------------ | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `messages` | string | Yes | JSON string containing the conversation messages array. Each message should have `role` (`user`, `assistant`, or `system`) and `content` properties. | | `model` | string | Yes | OpenAI model to use (e.g., `gpt-4`, `gpt-3.5-turbo`, `gpt-4-turbo`) | | `tools` | string | No | JSON string containing the tools array for function calling (optional) | | `reasoning_effort` | string | No | Reasoning effort for reasoning models: `none`, `minimal`, `low`, `medium`, `high` or `xhigh`. | | `api` | code | No | OpenAI endpoint to call: `chat` or `responses`. Defaults to `chat`, except that `tools` with a `reasoning_effort` other than `none` use `responses`. | #### Output Parameters | Name | Type | Description | | ------------ | ------ | ----------------------------------------------------------------------------- | | `content` | string | The AI-generated response text | | `tool_calls` | string | JSON string containing tool calls array (if the AI decided to call functions) | ### Basic Usage #### Simple Conversation Here's a basic example without function calling: ```bash curl 'https://api.medplum.com/fhir/R4/$ai' \ -X POST \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "messages", "valueString": "[{\"role\":\"user\",\"content\":\"What is FHIR?\"}]" }, { "name": "model", "valueString": "gpt-4" } ] }' ``` **Response**: ```json { "resourceType": "Parameters", "parameter": [ { "name": "content", "valueString": "FHIR (Fast Healthcare Interoperability Resources) is a standard for exchanging healthcare information electronically..." } ] } ``` #### Multi-Turn Conversation To maintain conversation context, include previous messages: ```json { "resourceType": "Parameters", "parameter": [ { "name": "messages", "valueString": "[{\"role\":\"user\",\"content\":\"What is FHIR?\"},{\"role\":\"assistant\",\"content\":\"FHIR is a standard...\"},{\"role\":\"user\",\"content\":\"What resources does it define?\"}]" }, { "name": "model", "valueString": "gpt-4" } ] } ``` ### FHIR Function Calling The AI operation supports function calling, enabling AI models to suggest FHIR operations based on user requests. #### Defining FHIR Tools Define a `fhir_request` tool to allow the AI to interact with FHIR resources: ```json { "type": "function", "function": { "name": "fhir_request", "description": "Make a FHIR request to the Medplum server. Use this to search, read, create, update, or delete FHIR resources.", "parameters": { "type": "object", "properties": { "method": { "type": "string", "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"], "description": "HTTP method for the FHIR request" }, "path": { "type": "string", "description": "FHIR resource path (e.g., 'Patient?name=Smith' or 'Patient/123')" }, "body": { "type": "object", "description": "FHIR resource to create or update (for POST, PUT, PATCH)" } }, "required": ["method", "path"] } } } ``` #### Example: AI-Suggested Patient Search **Request**: ```bash curl 'https://api.medplum.com/fhir/R4/$ai' \ -X POST \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "messages", "valueString": "[{\"role\":\"user\",\"content\":\"Find patient with phone 718-564-9483\"}]" }, { "name": "model", "valueString": "gpt-4" }, { "name": "tools", "valueString": "[{\"type\":\"function\",\"function\":{\"name\":\"fhir_request\",\"description\":\"Make a FHIR request...\",\"parameters\":{...}}}]" } ] }' ``` **Response**: ```json { "resourceType": "Parameters", "parameter": [ { "name": "content", "valueString": "I'll search for the patient with that phone number." }, { "name": "tool_calls", "valueString": "[{\"id\":\"call_123\",\"type\":\"function\",\"function\":{\"name\":\"fhir_request\",\"arguments\":{\"method\":\"GET\",\"path\":\"Patient?phone=718-564-9483\"}}}]" } ] } ``` #### Example: AI-Suggested Resource Creation **Request** (asking AI to create a task): ```json { "resourceType": "Parameters", "parameter": [ { "name": "messages", "valueString": "[{\"role\":\"user\",\"content\":\"Create a task to fill up chart note\"}]" }, { "name": "model", "valueString": "gpt-4" }, { "name": "tools", "valueString": "[{\"type\":\"function\",\"function\":{\"name\":\"fhir_request\",\"description\":\"Make a FHIR request...\",\"parameters\":{...}}}]" } ] } ``` **Response**: ```json { "resourceType": "Parameters", "parameter": [ { "name": "content", "valueString": "I'll create a task for filling up the chart note." }, { "name": "tool_calls", "valueString": "[{\"id\":\"call_xyz\",\"type\":\"function\",\"function\":{\"name\":\"fhir_request\",\"arguments\":{\"method\":\"POST\",\"path\":\"Task\",\"body\":{\"resourceType\":\"Task\",\"status\":\"requested\",\"intent\":\"order\",\"description\":\"Fill up chart note\"}}}}]" } ] } ``` ### Streaming Responses For real-time conversational interfaces, you can enable streaming by setting the `Accept` header: ```bash curl 'https://api.medplum.com/fhir/R4/$ai' \ -X POST \ -H "Content-Type: application/fhir+json" \ -H "Accept: text/event-stream" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "messages", "valueString": "[{\"role\":\"user\",\"content\":\"Explain FHIR resources\"}]" }, { "name": "model", "valueString": "gpt-4" } ] }' ``` The response will be in Server-Sent Events (SSE) format: ``` data: {"content":"FHIR"} data: {"content":" resources"} data: {"content":" are"} data: [DONE] ``` **Note**: Tool calls are not supported in streaming mode. ### Using with TypeScript SDK ```typescript import { MedplumClient } from '@medplum/core'; const medplum = new MedplumClient(); // Simple conversation const response = await medplum.fhirUrl('$ai').post({ resourceType: 'Parameters', parameter: [ { name: 'messages', valueString: JSON.stringify([{ role: 'user', content: 'What is FHIR?' }]), }, { name: 'model', valueString: 'gpt-4', }, ], }); const content = response.parameter?.find((p) => p.name === 'content')?.valueString; console.log(content); ``` #### With Function Calling ```typescript const fhirTools = [ { type: 'function' as const, function: { name: 'fhir_request', description: 'Make a FHIR request to the Medplum server...', parameters: { type: 'object' as const, properties: { method: { type: 'string' as const, enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], }, path: { type: 'string' as const, description: 'FHIR resource path', }, body: { type: 'object' as const, description: 'Resource body for POST/PUT/PATCH', }, }, required: ['method', 'path'], }, }, }, ]; const response = await medplum.fhirUrl('$ai').post({ resourceType: 'Parameters', parameter: [ { name: 'messages', valueString: JSON.stringify([{ role: 'user', content: 'Create a patient named John Smith' }]), }, { name: 'model', valueString: 'gpt-4', }, { name: 'tools', valueString: JSON.stringify(fhirTools), }, ], }); // Check for tool calls const toolCallsParam = response.parameter?.find((p) => p.name === 'tool_calls'); if (toolCallsParam?.valueString) { const toolCalls = JSON.parse(toolCallsParam.valueString); console.log('AI suggested:', toolCalls); // Execute the suggested FHIR operation for (const call of toolCalls) { const { method, path, body } = call.function.arguments; // Implement your logic to execute the FHIR request } } ``` ### Security Considerations 1. **API Key Management**: The OpenAI API key is stored as a project secret (`OPENAI_API_KEY`) and never exposed to clients. It is read server-side from project settings. 2. **Feature Gating**: The AI feature must be explicitly enabled on your project. 3. **Tool Execution**: The `$ai` operation returns suggested tool calls but does not execute them. Your application is responsible for: - Validating tool call parameters - Checking user permissions - Executing FHIR operations safely 4. **Rate Limiting**: Consider implementing rate limiting to prevent excessive OpenAI API usage and costs. 5. **Input Validation**: Always validate and sanitize user input before passing it to the AI operation. ### Error Handling #### API Key Not Configured ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "OpenAI API key not configured in project secrets" } } ] } ``` #### Invalid OpenAI API Key ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "Failed to call OpenAI API: OpenAI API error: 401 Unauthorized - Incorrect API key provided" } } ] } ``` #### Feature Not Enabled ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "forbidden", "details": { "text": "Forbidden" } } ] } ``` ### Limitations - **Streaming Mode**: Tool calls are not supported when streaming is enabled - **Model Support**: OpenAI models, or any model exposed through an OpenAI-compatible endpoint such as a LiteLLM proxy (configured via the `LLM_BASE_URL` secret) - **Tool Execution**: The operation returns suggested tool calls but does not execute them automatically ### Common Use Cases 1. **AI-Assisted Patient Search**: Use natural language to search for patients 2. **Clinical Documentation**: Generate structured clinical notes from conversational input 3. **FHIR Resource Creation**: Create FHIR resources through natural language commands 4. **Data Extraction**: Extract structured data from unstructured clinical text 5. **Patient Communication**: Build AI-powered chatbots for patient engagement 6. **Clinical Decision Support**: Provide AI-assisted recommendations based on patient data ### Related Documentation - [FHIR Operations Framework](/docs/api/fhir/operations) - [OpenAI Function Calling](https://platform.openai.com/docs/guides/function-calling) - [Medplum Project Features](/docs/access/projects) - [AI and Medplum](/docs/ai) - [Spaces in Medplum Provider](/docs/provider/spaces) — a worked example of `$ai` plus tool calling driving an in-app assistant --- ## AWS Textract/Comprehend This documentation explains how Medplum integrates with **AWS Textract** for optical character recognition (OCR) and **AWS Comprehend Medical** for extracting clinical entities from text. This powerful combination allows developers to process and analyze unstructured medical documents, such as scanned PDFs or images, directly within their Medplum projects. #### Overview Medplum's integration with AWS Textract and AWS Comprehend is implemented as a custom operation on FHIR `Media` and `DocumentReference` resources. This integration is designed to handle asynchronous processing of documents, making it ideal for large files. The workflow is as follows: 1. A `Media` or `DocumentReference` resource is created, referencing a `Binary` resource that contains the document (e.g., a PDF, JPEG, or PNG). 2. A `POST` request is sent to the `$aws-textract` operation on the `Media` or `DocumentReference` resource. 3. Medplum sends the document to AWS Textract for text detection. Textract processes the document and returns a job ID. 4. Medplum polls Textract using the job ID until the processing is complete. 5. Once Textract successfully processes the document, Medplum stores the raw JSON output as a new `Binary` and `Media` resource. 6. If the request includes the `comprehend: true` option, Medplum extracts the text from the Textract output and sends it to AWS Comprehend Medical for entity detection. 7. The raw JSON output from AWS Comprehend is also stored as a new `Binary` and `Media` resource. 8. The final result of the Textract operation is returned in the API response. This approach ensures that the original document remains untouched while the processed outputs (Textract and Comprehend JSON) are persistently stored as new FHIR resources. This allows for easy retrieval and auditing of the OCR and NLP results. #### Prerequisites To use this feature, your Medplum project must have the `aws-textract` feature flag enabled. This can be configured in your project settings. Additionally, your Medplum server must be configured to use **AWS S3 storage** for binaries, as Textract requires an S3 object location to process documents. #### Usage The integration is exposed via the `$aws-textract` operation on `Media` and `DocumentReference` resources. ##### Request To start the text and entity detection process, send a `POST` request to one of the following endpoints: **For Media resources:** `POST /fhir/R4/Media//$aws-textract` **For DocumentReference resources:** `POST /fhir/R4/DocumentReference//$aws-textract` The `` is the unique identifier of the `Media` or `DocumentReference` resource containing the document you wish to process. The request body is optional and can be used to enable the AWS Comprehend Medical integration. - **To run Textract only:** ```json {} ``` - **To run both Textract and Comprehend Medical:** ```json { "comprehend": true } ``` ##### Response The API will return the raw JSON response from AWS Textract. The response will be a JSON object that includes an array of `Blocks` representing the detected text, tables, and forms in the document. A successful response will have a status code of `200 OK`. Simultaneously, Medplum will have created new `Binary` and `Media` resources for the Textract output. If Comprehend was enabled, additional resources will be created for its output as well. These resources will have a `subject` field that references the original `Media` or `DocumentReference` resource, creating a clear link between the input document and its processed outputs. ##### Examples Here are examples of how to use the integration with both `Media` and `DocumentReference` resources: ###### Media Resource Example ```typescript // Import necessary types and client import { BotEvent, MedplumClient } from '@medplum/core'; import { Media } from '@medplum/fhirtypes'; /** * Example function to send a Media resource to the `$aws-textract` operation. * @param medplum The Medplum client instance. * @param event The BotEvent containing the Media resource. * @returns A string containing all the detected text, separated by newlines. */ export async function handler(medplum: MedplumClient, event: BotEvent>): Promise { const media = event.input; // Post the Media to the `$aws-textract` operation. // The second argument is the request body, here we enable Comprehend. const textractResponse = await medplum.post(medplum.fhirUrl('Media', media.id as string, '$aws-textract'), { comprehend: true, }); // The response contains the raw AWS Textract output. // You can now process this output as needed. const lines = textractResponse.Blocks.map((b: { Text?: string }) => b.Text).filter(Boolean); console.log(lines); return lines.join('\n'); } ``` ###### DocumentReference Resource Example ```typescript // Import necessary types and client import { MedplumClient } from '@medplum/core'; import { DocumentReference } from '@medplum/fhirtypes'; /** * Example function to send a DocumentReference resource to the `$aws-textract` operation. * @param medplum The Medplum client instance. * @param docRefId The ID of the DocumentReference resource. * @returns A string containing all the detected text, separated by newlines. */ export async function processDocumentReference(medplum: MedplumClient, docRefId: string): Promise { // Post the DocumentReference to the `$aws-textract` operation. // The second argument is the request body, here we enable Comprehend. const textractResponse = await medplum.post( medplum.fhirUrl('DocumentReference', docRefId, '$aws-textract'), { comprehend: true, } ); // The response contains the raw AWS Textract output. // You can now process this output as needed. const lines = textractResponse.Blocks.map((b: { Text?: string }) => b.Text).filter(Boolean); console.log(lines); return lines.join('\n'); } ``` These examples demonstrate how to call the `$aws-textract` operation on both `Media` and `DocumentReference` resources and process the resulting text. They're great starting points for building more complex workflows, such as extracting specific data points from a document and mapping them to other FHIR resources. ###### Key Differences Between Media and DocumentReference - **Media**: Represents captured or recorded content (images, videos, documents) - **DocumentReference**: A "pointer" to external documents, used for indexing and searching - **Binary**: The actual file data storage (referenced by both Media and DocumentReference) Both resource types can reference the same `Binary` resource, so you can use either approach depending on your use case. If you're working with documents that need OCR processing, you can now use either `Media` or `DocumentReference` resources directly. --- ## Build with AI on Medplum ### The Reality of Healthcare AI > _The barrier to production isn’t the AI model; it’s the lack of a secure, auditable foundation for healthcare data._ AI capabilities are improving quickly, and many applications in healthcare are now practical. Models can summarize clinical text, assist with documentation, and support analysis across large datasets. Healthcare, however, places constraints on AI that do not exist in most other industries. Systems handle protected health information, support regulated workflows, and influence clinical decisions. AI must be secure, auditable, and safe by design. Most healthcare AI efforts struggle not because of model quality, but because the foundations are weak. Data access is inconsistent, permissions are unclear, and integrations rely on manual processes or fragile pipelines. This is why many promising prototypes fail to reach production. Sustainable healthcare AI depends on getting the basics right: structured data, reliable interfaces, explicit access controls, and interoperability with the healthcare ecosystem. ### Healthcare AI Is an Infrastructure Problem > _Reliable AI requires a platform that treats data access, standardization, and security as core infrastructure rather than afterthoughts._ In practice, healthcare AI is an infrastructure problem. AI systems retrieve data, trigger workflows, and interact with users and external systems. Their reliability and safety depend on the platform they run on. Many organizations try to build this infrastructure in-house and get buried by a “maintenance tax.” Custom HL7 mapping, HIPAA/SOC 2 controls, and proprietary schemas create ongoing operational load that slows down actual AI development. Across real deployments, four requirements consistently matter: - **Programmatic Data Access:** AI systems need complete access through stable APIs. Partial integrations and one-off exports do not scale. - **Standardized Data Models:** Healthcare data must be structured consistently. Standards reduce ambiguity and support interoperability over time. - **Explicit Guardrails:** AI must operate within defined permissions. It should be possible to specify what an automated system can read, suggest, or change. - **Built-In Interoperability:** AI depends on labs, medications, billing, and messaging. Without native interoperability, AI remains limited. Medplum is designed as a healthcare developer platform with these requirements in mind, allowing teams to focus on clinical logic rather than rebuilding basic plumbing. ### API-First by Design: Why AI Needs Programmatic Access > _AI can only automate what it can access; true programmatic interfaces eliminate the fragile “integration phase” that kills most projects._ AI systems depend on reliable programmatic access to data. They retrieve information, evaluate context, and trigger workflows continuously—not through manual exports or periodic batch jobs. Many systems describe themselves as “API-enabled,” but APIs are often incomplete, inconsistent, or secondary to the user interface. Important data may only be accessible through internal workflows or manual intervention. Medplum is API-first by design. All core functionality is exposed through documented APIs, with no separate internal pathways. This has practical consequences: - AI systems can access the full clinical and operational context they need - Automation does not depend on fragile UI-driven processes - Background jobs and agents are straightforward to implement - The same foundations used to build applications are used to build automation and AI workflows ### Why FHIR Matters for AI (More Than People Realize) > _FHIR provides the predictable, semantically rich structure that allows modern LLMs to reason about clinical data with high accuracy._ Over the past decade, the healthcare industry has converged on FHIR as the common language for clinical data exchange. For AI, standardized structure matters. Models perform best when data is predictable, semantically rich, and consistently shaped. Because FHIR is a global standard, modern language models are already familiar with its structures and terminology through extensive documentation and real-world usage. This makes it easier for AI systems to reason about healthcare data without relying on brittle, organization-specific schemas. FHIR also provides a clear target for AI-driven data extraction. Many healthcare inputs—clinical notes, scanned documents, faxes, and messages—are unstructured by nature. AI systems are increasingly effective at transforming this information into structured representations. When those outputs map cleanly into FHIR resources, the result is data that can be validated, audited, and reused across workflows. This contrasts with proprietary models, where design shortcuts—such as hard-coded enums—become liabilities as systems scale. FHIR’s value is stability. ### Guardrails Are Not Optional: Building Safe AI in Healthcare > _Responsible AI depends on fine-grained permissions that treat automated agents with the same rigorous access controls as human clinicians._ Unlike traditional software, AI-driven workflows can make suggestions or initiate actions based on probabilistic reasoning. Without explicit guardrails, this creates unacceptable risk. A common pattern is **“can suggest, but not act.”** An AI may draft a note or recommend an order, while a human remains responsible for the final action. Supporting this requires permissions that are precise, enforceable, and auditable. In Medplum, an AI agent is governed by the same policy framework as a human user. Every action taken by an AI system is captured in a FHIR-standard `AuditEvent` log, providing a verifiable record of what data it accessed and what it suggested—critical for clinical safety and regulatory compliance. ### Open Source Makes AI Practical > _AI systems work best when the platform they operate on is transparent, well-documented, and widely understood._ Open source provides a practical advantage for healthcare AI that is often overlooked. Modern language models are trained on public codebases, documentation, and examples. Because Medplum is open source, its APIs, data models, and architectural patterns are already familiar to these systems. This makes it easier for AI tools to generate correct integrations, follow established best practices, and avoid unsafe assumptions. This transparency also benefits human developers. Teams can inspect how the platform works, understand how data flows, and reason about system behavior without relying on opaque abstractions or proprietary tooling. In healthcare AI, where correctness and auditability matter, this matters more than speed alone. Open source reduces guesswork, improves debuggability, and makes both human and automated systems easier to trust. ### Interoperability Is the Difference Between a Demo and Production > _To move beyond isolated demos, AI must be able to trigger actions across the entire ecosystem—from labs and pharmacy to billing and messaging._ Most healthcare AI systems work well in isolation, looking impressive in demos but stalling in real clinical environments. Real healthcare AI must interact with labs, medications, billing systems, and external partners. Point solutions struggle because they are built around a single data source and rely on custom integrations to reach everything else. Medplum functions as an interoperability hub, providing a unified data model and direct integration with HL7 v2, DICOM, and other industry standards. When interoperability is built into the platform, AI systems are not confined to isolated use cases; they can participate in the end-to-end workflows that define success in production. ### What This Enables in Practice > _Industry leaders like Rad AI and Unity AI use Medplum to bypass infrastructure builds and go straight to scaling AI-driven clinical workflows._ API-first design, standardized data, strong guardrails, and deep interoperability enable classes of AI systems that are otherwise difficult to build safely. - [Rad AI](/blog/radai-case-study): Builds tools that help radiologists by automating documentation and surfacing clinical context. By using Medplum for underlying infrastructure, they can focus on AI differentiation. They now support more than half of all U.S. medical imaging providers. - [Unity AI](/blog/scheduling-agents-unity-ai): Uses AI-driven agents to handle tens of thousands of patient outreach calls daily. Because Medplum integrates clinical data, scheduling, and messaging, Unity AI can operate agents across multiple systems without stitching together fragile point integrations. ### A Foundation, Not a Bet on a Single Model > _Models and regulations will change, but a foundation built on open standards ensures your AI strategy remains durable and adaptable._ AI models will continue to change; capabilities will improve and costs will shift. What should not change is the foundation those systems are built on. In healthcare, long-term success depends on platforms that are stable, interoperable, and designed with safety in mind. Medplum focuses on durable infrastructure: standardized data, comprehensive APIs, and strong access controls. This allows organizations to adopt new AI capabilities as they mature—without repeatedly re-architecting their core systems. ### AI Solutions with Medplum Medplum helps you put AI to work on real-world problems. Whether you're structuring clinical notes or extracting key data from faxes, Medplum is the platform that makes it possible. Explore our dedicated integrations to learn more: - **[Medplum `$ai` ](/docs/ai/ai-operation):** The FHIR-compliant operation for integrating with large language models (LLMs) and enabling conversational AI within healthcare applications. - **[Medplum AI (MCP)](/docs/ai/mcp):** Connect with Medplum's MCP integration to enable AI models to securely access and interact with healthcare data using FHIR standards. - **[Spaces (Medplum Provider)](/docs/provider/spaces):** An example implementation of an in-app AI assistant in the Provider app, built on `$ai` and a four-bot pipeline that translates natural language into FHIR API calls, summarizes results, and renders generated charts. - **[AWS Textract](/docs/ai/aws):** Extract and analyze text from clinical documents, faxes, and other unstructured data sources. - **[AWS Comprehend Medical](/docs/ai/aws):** Uncover valuable insights from clinical text, identifying medical conditions, medications, and treatments. Ready to see how Medplum can accelerate your AI journey? Get started with our documentation and a code example today. --- ## Medplum MCP Welcome to the official documentation for the Medplum MCP integration. This guide provides an in-depth look at how to set up and use the integration, detailing the available tools and providing practical examples to help you get started. Medplum is the open-source healthcare developer platform. Our MCP integration allows AI models to securely access, analyze, and interact with healthcare data (e.g., patient records, appointments, lab results) stored on a Medplum server, all powered by the FHIR standard. #### Why FHIR Makes This Powerful You might expect a healthcare integration to expose dozens of specialized, hand-written tools—one for scheduling, one for lab results, one for medications, and so on. The Medplum MCP takes the opposite approach, and the reason is FHIR. FHIR is an open, widely-adopted standard, and there is an enormous amount of high-quality FHIR content on the open internet: the official specification, implementation guides, worked examples, community Q&A, and reference code. As a result, modern LLMs are already deeply familiar with FHIR. They understand its resource types, how to construct search queries, how resources reference one another, and how to build valid request bodies—without any Medplum-specific instruction. This means we don't have to teach the model healthcare. We just have to give it a way to talk to the FHIR server. By exposing a single, general-purpose FHIR request tool (see [`fhir-request`](#fhir-request) below), we let the model bring its existing, extensive knowledge of FHIR to bear on your data. The model already knows _how_ to ask good questions of a FHIR server; the MCP simply lets it ask. That combination—a well-understood standard plus a direct, low-level tool—is what makes the Medplum MCP so capable despite its small surface area. #### Getting Started To begin using the Medplum MCP integration, follow these steps: 1. **Open Claude.ai:** You will need a paid plan to add integrations. 2. **Navigate to Settings:** Click on the settings icon in the bottom left, then navigate to "Organization Integrations." 3. **Add Integration:** Click "Add Integration" and enter the following details: - **Integration Name:** Medplum - **Integration URL:** https://api.medplum.com/mcp/stream 4. **Connect:** Back on the Organization integrations page, click the "Connect" button. You will be redirected to Medplum to authenticate. 5. **Confirm Access:** Once redirected back to Claude, you can create a new chat and confirm the integration is working by asking: "Can you please confirm you have access to the 'fhir-request' MCP tool?" _Note: LLMs can sometimes cache sessions. If you experience issues, try disconnecting and reconnecting the integration in a new chat._ #### Core Functionality and Tools The Medplum MCP integration exposes exactly **three** tools. This small, deliberate surface area is by design—as described [above](#why-fhir-makes-this-powerful), the model already understands FHIR, so it needs only a direct way to talk to the FHIR server rather than a large catalog of narrow, purpose-built tools. The three tools are: - [`fhir-request`](#fhir-request) — the primary tool, and the one that does the real work. - [`search`](#search-and-fetch) and [`fetch`](#search-and-fetch) — lightweight compatibility tools required by some clients. ##### `fhir-request` - **Title:** Perform a FHIR API Request - **Description:** This is a powerful, low-level tool that performs a direct FHIR API request. It can be used to create, read, update, or delete FHIR resources, providing full CRUD (Create, Read, Update, Delete) functionality. Because it maps directly onto the FHIR REST API, the model can use its existing FHIR knowledge to drive it—this is the tool behind nearly every interaction. - **Annotations:** This tool can modify data. - **Schema:** ```json { "type": "object", "properties": { "method": { "type": "string", "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"], "description": "The HTTP method for the request." }, "path": { "type": "string", "description": "The FHIR path, relative to the server's FHIR R4 base URL (e.g., 'Patient' or 'Patient?name=Smith'). Do not include a leading slash or the '/fhir/R4' prefix." }, "body": { "type": "object", "description": "The JSON body of the FHIR resource to be created or updated." } }, "required": ["method", "path"] } ``` ##### `search` and `fetch` - **Description:** These are lightweight, standard tools that some MCP clients (for example, ChatGPT) expect every server to implement in order to connect. They exist primarily for client compatibility—the bulk of real work is done through [`fhir-request`](#fhir-request), which can perform searches and reads directly against the FHIR API. #### Example Use Cases Here are three examples demonstrating the core capabilities of the Medplum MCP integration. These examples will be visible to users in the Anthropic Directory listing. **Example 1: Finding High Blood Pressure Patients (Using fhir-request)** - **Prompt:** "What are the names of all patients who have a blood pressure observation with a systolic value greater than 140 in the last year?" - **Tool Call:** The AI will use the `fhir-request` tool with the method set to "GET" and a path that queries the `Observation` resource with the appropriate FHIR search parameters (for example, filtering by LOINC code and value), then follows the references back to the associated `Patient` resources. - **Outcome:** The Medplum server responds with a list of patient names and IDs that match the criteria, which the AI can then summarize for the user. **Example 2: Scheduling a New Appointment (Using fhir-request)** - **Prompt:** "Create a new appointment for patient 'Jane Doe' with Dr. Smith for a routine check-up next Tuesday at 10 AM." - **Tool Call:** The AI will use the `fhir-request` tool with the method set to "POST", the path set to `Appointment`, and a body containing the details of the new appointment. - **Outcome:** The Medplum server creates the appointment and returns a success message, which the AI can confirm with the user. **Example 3: Fetching Specific Lab Results (Using fhir-request)** - **Prompt:** "What were the results of patient 'John Doe's most recent lab work for cholesterol?" - **Tool Call:** The AI will use the `fhir-request` tool with the method set to "GET"—first querying the `Patient` resource by name to find John Doe's ID, then querying the `Observation` resource filtered by that patient and the relevant cholesterol code, sorted to return the most recent results. - **Outcome:** The Medplum server returns the detailed observation data, including values and units, which the AI can present to the user in an easy-to-understand format. #### Authentication and Security Medplum uses **OAuth 2.0 with the 6/18 auth spec** to securely authenticate users. When you first connect the integration, you will be redirected to the Medplum server to log in and authorize Claude.ai to access your data. Medplum's platform ensures all data access is secure and compliant with relevant healthcare regulations. Our full privacy policy can be found here: https://www.medplum.com/privacy #### Support and Community For questions, feedback, or technical support, please contact us through one of our community channels. We recommend our Discord for real-time conversation and GitHub for bug reports. - **Discord:** https://discord.gg/medplum - **Support Email:** support@medplum.com --- ## Analytics When designing a healthcare analytics program, data quality and integrity are critical. To create a robust analytics program, constant monitoring of data quality and can be enabled and will be required to ensure that reports are meaningful and accurate. The following features should be used in concert to build out an analytics program: 1. Store data in the [FHIR Datastore](/docs/fhir-datastore) with emphasis on standard fields, especially on `Patient`, `Observation` and other common resources. 2. Use [Bots](/docs/bots) and [Subscriptions](/docs/subscriptions) to help maintain quality and correctness in real time, for example, ensuring all `Encounter.type` is tagged with the appropriate ontology, if not throw an error. 3. Export to a data warehouse for the workloads that aggregate across a whole population. Medplum synchronizes FHIR resources to [Snowflake](/docs/analytics/snowflake), [Amazon Redshift](/docs/analytics/redshift), and other engines that read open Apache Iceberg tables. Analytics on healthcare data generally have two broad areas of application: **retroactive analysis** of performance and quality metrics, and **predictive modeling** to make recommendations for future behavior. Retroactive analysis can be used to measure metrics related to operational efficiency and quality of care. In addition, many payors, including Medicare and Medicaid, provide higher reimbursements to providers who report specific [quality-of-care measures](/docs/compliance/onc#materials-and-usage). On the predictive side, Clinical Decision Support (CDS) systems encode evidence-based clinical guidelines into rules-based suggestions to guide clinicians. Machine learning can also be used to identify common patterns across patients and care plan recommendations. ### Program Design When designing your analytics program, it can be useful to consider the following categorization. | Program Type | Application Area | Implementation Tools | | ----------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------- | | Ad-hoc clinical reports | Retrospective | Data Warehouse: [Snowflake](/docs/analytics/snowflake), [Amazon Redshift](/docs/analytics/redshift) | | Healthcare standard reports (e.g. HEDIS, CMS Queries) | Retrospective | Data Warehouse: [Snowflake](/docs/analytics/snowflake), [Amazon Redshift](/docs/analytics/redshift) | | Clinical decision support | Prospective | [CDS Hooks](/docs/integration/cds-hooks) | | Machine Learning, predictive modeling | Prospective | Data Warehouse combined with [`$ai`](/docs/ai/ai-operation), [MCP](/docs/ai/mcp) and [Bots](/docs/bots) | ### Ad-Hoc Clinical Reports A common pattern for ad-hoc reports is to first ingest FHIR resources into the data warehouse as raw JSON, and then flatten the relevant fields in a second ETL stage. The Data Warehouse Connector, an optional add-on for paid plans, can run this synchronization for you on a schedule, writing FHIR resources to open Apache Iceberg tables that Snowflake, Amazon Redshift, Amazon Athena, and other engines read directly. See [Snowflake](/docs/analytics/snowflake) or [Amazon Redshift](/docs/analytics/redshift) for the table layout, query patterns, and how to get set up. ### Healthcare Standard Reports Healthcare standard reports rely on coding systems to classify conditions, procedures, drugs, and outcomes. These codes help create a standardized vocabulary between providers, labs, pharmacies, and payors to streamline operations, billing, and analysis. The U.S. healthcare system implements a number of different coding standards that have different specializations. See [Commonly Used Terminologies](/docs/terminology/common-terminologies) for the FHIR `system` URLs and ValueSets behind each one, and [Terminologies and Coded Values](/docs/terminology) for how Medplum stores and expands them. - **RxNorm:** A code system of normalized drug names, organized into a hierarchical ontology to represent generics, branded drugs, single doses and drug packs. - **LOINC:** Clinical terminology relevant to clinical lab orders and results. - **SNOMED/ICD-10/CPT/HCPCs:** Multiple code sets used to represent clinical procedures and diagnoses. SNOMED CT serves as a global standard for clinical terminology in a variety of contexts, while CPT, ICD-10, and HCPCS are primarily used for billing insurance. To help organize these different standards, the NIH maintains the [Unified Medical Language System](https://www.nlm.nih.gov/research/umls/index.html) (UMLS), which provides a unified package with codes and terminologies from all major medical code systems, as well as conceptual maps between the various standards. In the domain of machine learning and analytics, using standardized codes makes it easier to share knowledge with researchers and leverage existing datasets, models, and clinical guidelines. Assuming you have standards compliant FHIR, tagged with the appropriate ontologies, standard reports become accessible. As much as possible, we encourage organizations to constantly evaluate data quality and run reports. #### HEDIS HEDIS Measures are a standardized set of over 90 metrics used by health plans to measure quality of care. In addition to helping digital health providers identify gaps in care, reporting HEDIS measures can allow providers to qualify for increased payor reimbursements through Pay for Quality or Value Based Care models. [Digital versions](https://store.ncqa.org/hedis-quality-measurement.html) of the HEDIS measures are published in FHIR/CQL format, and having patient data modeled natively as FHIR allows providers to easily compute and report these measures. #### CMS Measures The Centers for Medicare & Medicaid Services (CMS) requires certain providers to report electronic clinical quality measures (eCQMs) via an EHR system that has been certified by the ONC. CMS publishes [eCQMs in FHIR/CQL format](https://ecqi.healthit.gov/ecqms) to help standardize computation of these measures across EMRs, and they provide the [Cypress tool](https://www.healthit.gov/cypress/index.html) to validate EHR implementations against synthetic patient data. Related: [eCQM fact sheet 2022](https://www.cms.gov/files/document/2022-ecqm-reporting-requirements.pdf), [List of CQMs](https://docs.google.com/spreadsheets/d/1OoEcFjiHXHfnZn0y3eQ5D7hjijpr0dop5ckEwnOnSmo/edit#gid=0) ### Clinical Decision Support Clinical Decision Support (CDS) systems encode clinical guidelines into rules-based suggestions to guide clinicians and standardize care. CDS rules can incorporate conditional logic based on patient demographic information and medical history to generate personalized clinical guidelines. CDS Hooks is an emerging standard, built on top of FHIR, that defines REST APIs for clients to request CDS care plans from hosted CDS services. CDS workflows are supported through the PlanDefinition resource (more in [Care Plans](/docs/careplans)). Each plan can then be instantiated as a CarePlan for a patient using the $apply operation. CDS systems are great for helping providers ensure consistent quality of care across individual physicians by providing standardized treatment recommendations and eliminating care gaps. For Medplum-specific endpoint contracts and implementation guidance, see [CDS Hooks](/docs/integration/cds-hooks). For simple CDS such as [social determinants of health risk-scoring](https://www.ajmc.com/view/social-determinants-of-health-score-does-it-help-identify-those-at-higher-cardiovascular-risk) or [eGFR](https://www.kidneyfund.org/all-about-kidneys/tests/blood-test-egfr) calculation [bots](/docs/bots) combined with [subscriptions](/docs/subscriptions) are a very fast and pragmatic choice. ### Machine Learning and AI Language models now cover much of what used to need a purpose-trained model: summarizing a chart, extracting structured data from a scanned document, drafting a note for review. Medplum exposes this through the [`$ai` operation](/docs/ai/ai-operation), which calls an LLM through the FHIR API and can return suggested FHIR operations rather than free text. [MCP](/docs/ai/mcp) gives an assistant scoped access to the same datastore, and [AWS Textract and Comprehend Medical](/docs/ai/aws) handle documents and faxes. Purpose-trained models still matter for risk scoring and forecasting, and they run the same way. Three things decide whether any of this reaches production: - **Training and evaluation sets come from the warehouse.** Assembling a cohort, holding out a test set, and scoring a model against it are aggregate queries, so they belong in [Snowflake](/docs/analytics/snowflake) or [Amazon Redshift](/docs/analytics/redshift). Inference belongs in a [Bot](/docs/bots), triggered by a [Subscription](/docs/subscriptions) when the resource being scored is written. Enroll the cohort itself with a [Group](/docs/api/fhir/resources/group). - **Scope what the model can touch.** An AI agent reads and writes through the same API as everything else, so [Access Policies](/docs/access/access-policies) decide what it can see and change. Set that boundary before the agent is useful, not after. - **Keep the clinician in control.** Physicians are slow to adopt models they cannot interrogate. Write the reasoning where it will actually be read: a [DetectedIssue](/docs/api/fhir/resources/detectedissue), a note on the order, or a `Communication` on the `Task` a reviewer picks up. A recommendation someone can accept or reject gets adopted faster than one that acts on its own. See [Build with AI on Medplum](/docs/ai) for the full picture. --- ## Amazon Redshift :::info[Paid Add-On] Redshift sync is available through the Data Warehouse Connector, an optional add-on for paid plans. A Medplum team member creates and enables the pipeline for your project. Contact us at [hello@medplum.com](mailto:hello@medplum.com) to get started, or see [pricing](/pricing). ::: Medplum synchronizes your FHIR data to [Apache Iceberg](https://iceberg.apache.org/) tables in [Amazon S3 Tables](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables.html) on a schedule, then grants your AWS account access to those tables through [AWS Lake Formation](https://docs.aws.amazon.com/lake-formation/latest/dg/data-filtering.html). [Amazon Redshift](https://docs.aws.amazon.com/redshift/latest/mgmt/serverless-workgroup-namespace.html) in your account queries them in place. There is no export job for your team to build or operate, and no copy of the data lands in a bucket you have to manage. Use this for population health reporting, quality measure calculation, cohort analysis, and any workload that aggregates across large numbers of resources. [FHIR search](/docs/search/basic-search) is designed for clinical lookups on individual patients, not for aggregation across a population. Redshift is one of several options. The sync writes open Iceberg tables rather than a Redshift-specific format, so the same tables can be queried from [Snowflake](/docs/analytics/snowflake), Amazon Athena, Apache Spark, or Trino. Two things make Redshift the recommendation when you are staying inside AWS: it can read the tables with no data movement, and it is the only AWS engine that supports the redacted view layer described below. Athena cannot do redaction. ### How it works ```mermaid flowchart LR A["Medplum FHIR Datastore"] --> B["Sync worker"] B --> C["Iceberg tables in Amazon S3"] C --> D["Lake Formation grant"] D --> E["Redshift in your AWS account"] ``` 1. A sync worker runs on a schedule you choose. It reads from a read replica, so production traffic is not affected. 2. Each run writes only the resource versions created or changed since the previous run. 3. Medplum grants an IAM role you own permission to read the tables for the Medplum projects you name. The grant carries a row filter on `project_id`, so the role cannot see projects outside the grant. 4. Your Redshift workgroup attaches to the shared catalog and queries the tables directly. Freshness is set by the schedule. Hourly is a common starting point. This pipeline is not built for sub-minute latency: to react to individual resources in real time, use [Bots](/docs/bots) and [Subscriptions](/docs/subscriptions). ### Table layout Medplum writes one table per FHIR resource type, named after the resource type in lowercase with a `_history` suffix. | FHIR resource type | Table | | ------------------ | --------------------- | | Patient | `patient_history` | | Observation | `observation_history` | | Encounter | `encounter_history` | | Condition | `condition_history` | Every table has the same five columns. | Column | Type | Description | | -------------- | --------- | --------------------------------------------------------- | | `id` | string | Resource id. Stable across every version of the resource. | | `version_id` | string | `meta.versionId` for this row. | | `content` | string | The complete FHIR resource as JSON. | | `last_updated` | timestamp | `meta.lastUpdated` for this row. | | `project_id` | string | The Medplum project that owns the resource. | Two things to know about the shape: **These are history tables.** Every version of every resource is its own row, so a Patient updated ten times contributes ten rows. Queries that want current state select the latest version per `id`. The pattern is below. **Resources arrive as JSON, not as columns.** `content` holds the whole FHIR resource. This is deliberate. FHIR resources are deeply nested and sparsely populated, so flattening at export time would either drop data or produce thousands of mostly empty columns. You flatten the fields you need in Redshift, where changing your mind costs a view definition instead of a re-export. ### Raw tables or redacted views The tables in S3 are a raw copy of Medplum's history tables, including the fields that hold secrets. You choose which of two surfaces gets shared with your account: - **Raw.** The Iceberg tables as written, every field intact. - **Redacted.** [Late-binding views](https://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_VIEW.html) over the same tables that null out a fixed list of secret fields using [`OBJECT_TRANSFORM`](https://docs.aws.amazon.com/redshift/latest/dg/r_OBJECT_TRANSFORM_function.html). The fields are `User.passwordHash`, `User.mfaSecret`, `ClientApplication.secret`, `IdentityProvider.clientSecret`, `Project.secret`, and `Project.systemSecret`. Everything else passes through untouched, including fields Medplum adds later. Redaction happens at query time in the view, not in the sync, so the list can change without a re-export and without downtime. Tell us which surface you want when you open the request. Most teams take the redacted views. This is the full list of what redaction does. It is not de-identification, it does not remove PHI, and it is not a substitute for access control on your side. See [Access control and compliance](#access-control-and-compliance). ### Querying in Redshift `content` arrives as a JSON string. Parse it into the [`SUPER`](https://docs.aws.amazon.com/redshift/latest/dg/r_SUPER_type.html) type with [`JSON_PARSE`](https://docs.aws.amazon.com/redshift/latest/dg/JSON_PARSE.html) and use [PartiQL navigation](https://docs.aws.amazon.com/redshift/latest/dg/query-super.html) to read into it. In the redacted views `content` is already `SUPER`, so skip the `JSON_PARSE` call. FHIR field names are camelCase, and Redshift lowercases attribute names in dot notation unless you turn on case-sensitive identifiers. Set this once per session before running anything below: ```sql SET enable_case_sensitive_identifier TO true; ``` If you would rather not change the session setting, [`JSON_EXTRACT_PATH_TEXT`](https://docs.aws.amazon.com/redshift/latest/dg/JSON_EXTRACT_PATH_TEXT.html) reads the same field and is always case-sensitive. On the raw tables `content` is a JSON string, so read it directly with `JSON_EXTRACT_PATH_TEXT(content, 'birthDate')`. On the redacted views `content` is already `SUPER`, so serialize it first: `JSON_EXTRACT_PATH_TEXT(JSON_SERIALIZE(content), 'birthDate')`. #### Current version of each resource Redshift has no `QUALIFY` clause, so filter on [`ROW_NUMBER`](https://docs.aws.amazon.com/redshift/latest/dg/r_WF_ROW_NUMBER.html) in an outer query. Views over external tables require `WITH NO SCHEMA BINDING`. ```sql create or replace view patient_current as select id, version_id, last_updated, project_id, resource from ( select id, version_id, last_updated, project_id, json_parse(content) as resource, row_number() over (partition by id order by last_updated desc) as rn from patient_history ) where rn = 1 with no schema binding; ``` The view examples on this page read the raw tables. On the redacted views `content` is already `SUPER`, so drop `json_parse` and select `content` directly. Deleted resources still appear, as a tombstone version carrying `meta.deleted`. Filter them out when you want live resources only: ```sql select * from patient_current where resource.meta.deleted is null; ``` See [Deleting Data](/docs/fhir-datastore/deleting-data) for what a tombstone contains. Keeping the tombstones is what lets you answer "when did this resource go away," so filter at the view boundary rather than asking to have them excluded from sync. #### Flattening fields Pull out the fields your report needs, casting each one to a Redshift type: ```sql select id, resource.birthDate::date as birth_date, resource.gender::varchar as gender, resource.name[0].family::varchar as family_name, resource.name[0].given[0]::varchar as given_name from patient_current where resource.meta.deleted is null; ``` Repeating elements expand by unnesting the array in the `FROM` clause. This returns one row per identifier: ```sql select p.id, i.system::varchar as identifier_system, i.value::varchar as identifier_value from patient_current p, p.resource.identifier i; ``` #### Worked example: numeric lab results ```sql create or replace view observation_current as select id, last_updated, project_id, resource from ( select id, last_updated, project_id, json_parse(content) as resource, row_number() over (partition by id order by last_updated desc) as rn from observation_history ) where rn = 1 with no schema binding; select o.id, o.resource.subject.reference::varchar as patient_reference, c.code::varchar as loinc_code, c.display::varchar as loinc_display, o.resource.valueQuantity.value::float as result_value, o.resource.valueQuantity.unit::varchar as result_unit, o.resource.effectiveDateTime::timestamp as effective_time from observation_current o, o.resource.code.coding c where o.resource.meta.deleted is null and c.system::varchar = 'http://loinc.org' and o.resource.valueQuantity.value is not null; ``` Reports like this are only as good as the coding in the underlying data. Use [Bots](/docs/bots) and [Subscriptions](/docs/subscriptions) to enforce coding at write time rather than repairing it in the warehouse. See [Analytics](/docs/analytics) for the coding systems involved. #### Multiple projects If your organization runs more than one Medplum project, filter on `project_id`. Build it into the view so that every downstream query inherits the scope: ```sql create or replace view patient_current as select ... from patient_history where project_id = '' ... with no schema binding; ``` This is a convenience for your own queries, not a security boundary. The Lake Formation row filter on the grant is what actually limits which projects your role can read. ### Getting set up A Medplum team member configures the sync and the grant. Your team creates the Redshift workgroup and the IAM role that will read the data. #### What to send us | Item | Notes | | ---------------------- | ------------------------------------------------------------------------------------------------- | | AWS account and region | Where your Redshift workgroup lives. Matching our region avoids cross-region transfer. | | IAM role ARN | The role you create in step 2 below. Medplum grants it read access. | | Medplum project ids | The projects you want in the warehouse. The grant is filtered to these. | | Raw or redacted | Which surface you want shared. See [Raw tables or redacted views](#raw-tables-or-redacted-views). | | Sync schedule | Hourly is a good default. Set it by how fresh your reports need to be. | | Resource types | All types by default. You can name an include list or an exclude list, but not both. | | Backfill start date | The earliest `meta.lastUpdated` to export. Omit it to load all history. | `AuditEvent` is worth a decision rather than a default. It is often the largest history table in a project, and it answers a different set of questions than clinical reporting does. Either exclude it or give it its own schedule. #### Step 1: Create a Redshift workgroup Create a [Redshift Serverless](https://docs.aws.amazon.com/redshift/latest/mgmt/serverless-workgroup-namespace.html) namespace and workgroup in your account. Defaults are fine. Provisioned RA3 clusters work too. Confirm you can connect and run a query against it before going further, so that any problem you hit later is on the sharing path rather than on the cluster. #### Step 2: Create an IAM role for Redshift Create one IAM role used only by Redshift. Trust policy: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": ["redshift.amazonaws.com", "redshift-serverless.amazonaws.com"] }, "Action": "sts:AssumeRole" } ] } ``` Permissions policy. `AmazonRedshiftAllCommandsFullAccess` is the console default and works. Least privilege is Glue catalog reads plus Lake Formation credential vending: ```json { "Version": "2012-10-17", "Statement": [ { "Sid": "GlueCatalogRead", "Effect": "Allow", "Action": [ "glue:GetDatabase", "glue:GetDatabases", "glue:GetTable", "glue:GetTables", "glue:GetPartition", "glue:GetPartitions", "glue:BatchGetPartition", "glue:GetUserDefinedFunction", "glue:GetUserDefinedFunctions" ], "Resource": "*" }, { "Sid": "LakeFormationCredentials", "Effect": "Allow", "Action": "lakeformation:GetDataAccess", "Resource": "*" } ] } ``` Attach the role to your namespace under **IAM roles** and mark it default, then send us the role ARN. #### Step 3: Accept the share Medplum grants your role read access through Lake Formation. The grant arrives in your account as an invitation in [AWS Resource Access Manager](https://docs.aws.amazon.com/ram/latest/userguide/what-is.html), with the catalog details in it. You do not need our AWS account id or our bucket names. #### Step 4: Create a resource link and a database Create a [resource link](https://docs.aws.amazon.com/lake-formation/latest/dg/resource-links-about.html) in your own Glue catalog pointing at the shared database, then grant `DESCRIBE` on the link to the identities that will query it. If your Redshift workgroup is in a different region from the shared catalog, set `Region` on the target database in the resource link. Then create the database in Redshift from the resource link ARN: ```sql CREATE DATABASE medplum_dw FROM ARN 'arn:aws:glue:::database/' WITH DATA CATALOG SCHEMA IAM_ROLE ''; ``` See [`CREATE DATABASE`](https://docs.aws.amazon.com/redshift/latest/dg/r_CREATE_DATABASE.html) for the full syntax. Confirm the tables are visible: ```sql SHOW TABLES FROM SCHEMA medplum_dw.; select project_id, count(*) from medplum_dw..patient_history group by 1; ``` Only the project ids in the grant should appear. If they do not, or the database is empty, tell us before changing anything on your side: the fix is almost always a grant on ours. Two things that will not work, by design. Only principals that can use both Redshift and Lake Formation can query the share, so local Redshift users created with `CREATE USER` and a password cannot see it. Use federated IAM identities. And you cannot create your own unfiltered grant on the shared tables. Project isolation is enforced on the Medplum side. Self-hosted deployments configure the same sync through the `dataWarehouse` block in [server config](/docs/self-hosting/server-config#datawarehouse). ### Access control and compliance :::warning[Full replication, no Access Policies] The warehouse is a full copy of your FHIR data. Every resource and every version in the projects you name is replicated, with no filtering by user, role, or compartment on the way out. Medplum [Access Policies](/docs/access/access-policies) do not apply to it. Once the data lands in Redshift, securing it is your responsibility. Your own roles, grants, and audit logging are the only controls on it. ::: Treat the warehouse as a PHI system: put it in scope for your BAA, your access reviews, and your audit logging. Three practices that follow from this: - **Grant on views, not on the history tables.** A view that filters `project_id`, drops tombstones, and selects only the fields a team needs is easier to reason about than a policy on raw FHIR JSON. - **De-identify in Redshift for analytics that do not need identity.** Cohort and quality measure work usually does not need names, addresses, or contact details. Build a de-identified view layer and point most consumers at that. - **Keep the row filter as the boundary between projects.** If you run separate dev, staging, and production projects, the Lake Formation grant is what separates them. A `WHERE project_id` in a view is a convenience on top of it. ### What this does not do - **It does not write back.** The sync is one directional. Changes made in Redshift never reach the FHIR datastore. - **It does not carry attachment bytes.** [Binary](/docs/api/fhir/resources/binary) resources sync as their FHIR JSON. The underlying file content stays in Medplum's storage service. - **It does not replace real-time workflows.** Anything that needs to happen within seconds of a resource changing belongs in a [Bot](/docs/bots). - **Redaction is not access control.** The redacted views null out six secret fields. They do not restrict who sees a patient. ### Other warehouses The tables are open Iceberg, so the engine is your choice. Everything on this page applies except the query syntax and the setup steps: the schedule, the table layout, the five columns, and the history semantics are the same. - **Snowflake** attaches to the Iceberg catalog directly. See [Snowflake](/docs/analytics/snowflake). - **[Amazon Athena](https://docs.aws.amazon.com/athena/latest/ug/what-is.html)** reads the same Lake Formation grant with no cluster to run. It cannot apply the redacted view layer, so it is only an option on the raw surface. - **Apache Spark** and **Amazon EMR** read the tables through the S3 Tables catalog. - **Trino** connects to the [S3 Tables Iceberg REST endpoint](https://aws.amazon.com/blogs/storage/query-amazon-s3-tables-from-open-source-trino-using-apache-iceberg-rest-endpoint) using its Iceberg connector with SigV4 authentication. Tell us which engine you are using when you open the request and we will point the catalog at it. If you are not using the Data Warehouse Connector, the [Bulk FHIR API](/docs/api/fhir/operations/bulk-fhir) exports resources as NDJSON that you can stage and load yourself. It runs on demand rather than on a schedule, and you own the loading and the incremental logic. ### See also - [Snowflake](/docs/analytics/snowflake) for the same pipeline read from Snowflake - [Analytics](/docs/analytics) for program design, coding systems, and standard measures - [Bulk FHIR API](/docs/api/fhir/operations/bulk-fhir) - [Access Policies](/docs/access/access-policies) - [Server config: dataWarehouse](/docs/self-hosting/server-config#datawarehouse) --- ## Snowflake :::info[Paid Add-On] Synchronization to [Snowflake](https://www.snowflake.com/) is available through the Data Warehouse Connector, an optional add-on for paid plans. A Medplum team member creates and enables the pipeline for your project and uses [Secure Data Sharing](https://docs.snowflake.com/en/user-guide/data-sharing-intro) to share the data with you. Contact us at [hello@medplum.com](mailto:hello@medplum.com) to get started, or see [pricing](/pricing). ::: Medplum synchronizes your FHIR data to [Apache Iceberg](https://iceberg.apache.org/) tables in [Amazon S3 Tables](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-tables.html) on a schedule. Snowflake reads those tables in place, so there is no export job for your team to build or operate. Use this for population health reporting, quality measure calculation, cohort analysis, and any workload that aggregates across large numbers of resources. [FHIR search](/docs/search/basic-search) is designed for clinical lookups on individual patients, not for aggregation across a population. Snowflake is one of several options. The sync writes open Iceberg tables rather than a Snowflake-specific format, so the same tables can be queried from Amazon Redshift, Apache Spark, or Trino. This page uses Snowflake for the examples. The setup on the Medplum side is the same for any of them — see [Other warehouses](#other-warehouses) for what differs in how the data is exposed. ### How it works ```mermaid flowchart LR A["Medplum FHIR Datastore"] --> B["Sync worker"] B --> C["Amazon S3 Tables (Iceberg)"] C --> D["Snowflake"] ``` 1. A sync worker runs every five minutes. It reads from a read replica, so production traffic is not affected. 2. Each run writes only the resource versions created or changed since the previous run. 3. Medplum shares the data with your Snowflake account as a private listing. Accepting it creates an imported database in your account, exposing the data as views with redacted secrets — see [View layout](#view-layout) below. This pipeline is not built for sub-minute latency: to react to individual resources in real time, use [Bots](/docs/bots) and [Subscriptions](/docs/subscriptions). ### View layout Medplum shares one view per FHIR resource type, in the `SHARE_REDACTED` schema, named after the resource type in lowercase with a `_history` suffix. | FHIR resource type | View | | ------------------ | ------------------------ | | Patient | `patient_history` | | Observation | `observation_history` | | Encounter | `encounter_history` | | Condition | `condition_history` | | ServiceRequest | `servicerequest_history` | There is one view per [FHIR resource type Medplum supports](https://www.medplum.com/docs/api/fhir/resources). List what you have access to, and inspect any view's columns, with: ```sql show views in schema share_redacted; describe view patient_history; ``` Every view has the same five columns. | Column | Type | Description | | -------------- |---------------| -------------------------------------------------------------- | | `id` | `UUID` | Resource id. Stable across every version of the resource. | | `version_id` | `UUID` | `meta.versionId` for this row. | | `content` | `VARIANT` | The complete FHIR resource as JSON. | | `last_updated` | `TIMESTAMPTZ` | `meta.lastUpdated` for this row. | | `project_id` | `UUID` | The Medplum project that owns the resource. | Two things to know about the shape: **These are history views.** Every version of every resource is its own row, so a Patient updated ten times contributes ten rows. Queries that want current state select the latest version per `id`. The pattern is below. **Resources arrive as JSON, not as columns.** `content` holds the whole FHIR resource. This is deliberate. FHIR resources are deeply nested and sparsely populated, so flattening at export time would either drop data or produce thousands of mostly empty columns. You flatten the fields you need by building your own views on top, where changing your mind costs a view definition instead of a re-export. **These are views with redacted secrets.** A few of them drop specific fields before Medplum shares the data — see [Redacted fields](#redacted-fields) below. ### Redacted fields | View | Fields removed from `content` | | ---------------------------- | -------------------------------- | | `user_history` | `passwordHash`, `mfaSecret` | | `clientapplication_history` | `secret` | | `identityprovider_history` | `clientSecret` | | `project_history` | `secret`, `systemSecret` | All other views keep the full FHIR JSON. ### Querying in Snowflake `content` type is `VARIANT`, and therefore you can use Snowflake's [semi-structured operators](https://docs.snowflake.com/en/user-guide/querying-semistructured) to read into it. Want to try these queries before your own sync is live? [Download a synthetic data sample](/examples/medplum-synthetic-data-sample.zip) that contains synthetic FHIR resources shaped the same way real data lands in this pipeline. #### Recent resources Filter on `last_updated` for a quick look at what changed recently, without building a view first: ```sql select id, last_updated, content:name[0]:family::string as family_name, content:birthDate::date as birth_date from patient_history where last_updated >= dateadd(day, -30, current_timestamp()) order by last_updated desc; ``` #### Current version of each resource Use [`QUALIFY`](https://docs.snowflake.com/en/sql-reference/constructs/qualify) to keep the newest row per `id`. ```sql create or replace view patient_current as select id, version_id, last_updated, project_id, content as resource from patient_history qualify row_number() over (partition by id order by last_updated desc) = 1; ``` Deleted resources still appear, as a tombstone version carrying `meta.deleted`. Filter them out when you want live resources only: ```sql select * from patient_current where resource:meta:deleted is null; ``` See [Deleting Data](/docs/fhir-datastore/deleting-data) for what a tombstone contains. Keeping the tombstones is what lets you answer "when did this resource go away," so filter at the view boundary rather than asking to have them excluded from sync. #### Flattening fields Pull out the fields your report needs: ```sql select id, resource:birthDate::date as birth_date, resource:gender::string as gender, resource:name[0]:family::string as family_name, resource:name[0]:given[0]::string as given_name from patient_current where resource:meta:deleted is null; ``` Repeating elements expand with [`LATERAL FLATTEN`](https://docs.snowflake.com/en/sql-reference/functions/flatten). This returns one row per identifier: ```sql select p.id, i.value:system::string as identifier_system, i.value:value::string as identifier_value from patient_current p, lateral flatten(input => p.resource:identifier) i; ``` #### Worked example: numeric lab results ```sql create or replace view observation_current as select id, last_updated, content as resource from observation_history qualify row_number() over (partition by id order by last_updated desc) = 1; select o.id, o.resource:subject:reference::string as patient_reference, c.value:code::string as loinc_code, c.value:display::string as loinc_display, o.resource:valueQuantity:value::float as result_value, o.resource:valueQuantity:unit::string as result_unit, o.resource:effectiveDateTime::timestamp as effective_time from observation_current o, lateral flatten(input => o.resource:code:coding) c where o.resource:meta:deleted is null and c.value:system::string = 'http://loinc.org' and o.resource:valueQuantity:value is not null; ``` Reports like this are only as good as the coding in the underlying data. Use [Bots](/docs/bots) and [Subscriptions](/docs/subscriptions) to enforce coding at write time rather than repairing it in the warehouse. See [Analytics](/docs/analytics) for the coding systems involved. #### Join across resource types Join on `project_id` plus a FHIR reference string. Join the `_current` views (not the raw history views) so a resource with many versions doesn't fan out the join: ```sql select o.id as observation_id, p.id as patient_id from observation_current o join patient_current p on p.project_id = o.project_id and o.resource:subject:reference::string = 'Patient/' || p.id; ``` #### Multiple projects Rows are limited to the Medplum projects mapped to your listing — you only see your own projects, never another customer's. If your organization runs more than one Medplum project, you might want filter on `project_id`. By default, all your projects are shared with Snowflake. Build it into the view so that every downstream query inherits the scope: ```sql create or replace view patient_current as select ... from patient_history where project_id = '' qualify row_number() over (partition by id order by last_updated desc) = 1; ``` An empty result set almost always means there are no rows yet for the projects mapped to your listing — not a missing or broken view. ### Getting set up A Medplum team member configures the sync. To open the request, send us: | Item | Notes | | ------------------- | ------------------------------------------------------------------------------------------------ | | AWS account and region | Where the S3 table bucket lives. Matching your Snowflake region avoids cross-region transfer. | | Sync schedule | Hourly is a good default. Set it by how fresh your reports need to be. | | Resource types | All types by default. You can name an include list or an exclude list, but not both. | | Backfill start date | The earliest `meta.lastUpdated` to export. Omit it to load all history. | | Snowflake account locator | [your data sharing identifier](https://docs.snowflake.com/en/user-guide/admin-account-identifier#label-account-name-data-sharing), in the form `ORGNAME.ACCOUNT_NAME` | `AuditEvent` is worth a decision rather than a default. It is often the largest history table in a project, and it answers a different set of questions than clinical reporting does. Either exclude it or give it its own schedule. On the Snowflake side, your team does the following things: 1. Accept the [Snowflake private listing](https://docs.snowflake.com/en/collaboration/collaboration-listings-about) from Medplum. See [Access and install listings as a consumer](https://docs.snowflake.com/en/collaboration/consumer-listings-access): - Sign in to Snowsight with a role that can get listings (`ACCOUNTADMIN`, or a role with `CREATE DATABASE` and `IMPORT SHARE`). - Go to **Data sharing** → **External sharing**. - On **Shared with you**, under **Privately shared listings**, open the Medplum listing and select **Get**. This creates an imported database in your account. - Note the database name Snowsight assigns, then point your session at it before running any of the queries on this page: ```sql use database ; use schema share_redacted; ``` 2. Configure roles and access controls, as per your requirements If you are not using the Data Warehouse Connector, the [Bulk FHIR API](/docs/api/fhir/operations/bulk-fhir) exports resources as NDJSON that you can stage and load yourself. It runs on demand rather than on a schedule, and you own the loading and the incremental logic. ### Access control and compliance :::warning[Full replication, no Access Policies] The warehouse is a full copy of your FHIR data. Every resource and every version in the projects you name is replicated, with no filtering by user, role, or compartment on the way out. Medplum [Access Policies](/docs/access/access-policies) do not apply to it. Once the data lands in Snowflake, securing it is your responsibility. Your own roles, grants, and audit logging are the only controls on it. ::: Treat the warehouse as a PHI system: put it in scope for your BAA, your access reviews, and your audit logging. Two practices that follow from this: - **Grant on your own views, not on the shared history views directly.** A view that filters `project_id`, drops tombstones, and selects only the fields a team needs is easier to reason about than a policy on raw FHIR JSON. - **De-identify in Snowflake for analytics that do not need identity.** Cohort and quality measure work usually does not need names, addresses, or contact details. Build a de-identified view layer and point most consumers at that. ### What this does not do - **It does not write back.** The sync is one directional. Changes made in Snowflake never reach the FHIR datastore. - **It does not carry attachment bytes.** [Binary](/docs/api/fhir/resources/binary) resources sync as their FHIR JSON. The underlying file content stays in Medplum's storage service. - **It does not replace real-time workflows.** Anything that needs to happen within seconds of a resource changing belongs in a [Bot](/docs/bots). ### Other warehouses The tables are open Iceberg, so the engine is your choice. Everything on this page applies except the query syntax and the redaction: the schedule, the five columns, and the history semantics are the same. Other engines read the raw Iceberg tables directly rather than the redacted views Snowflake gets through the private listing — see [View layout](#view-layout) and [Redacted fields](#redacted-fields). - **Amazon Redshift** reads S3 Tables through the AWS Glue Data Catalog integration. - **Apache Spark** and **Amazon EMR** read them through the S3 Tables catalog. - **Trino** connects to the [S3 Tables Iceberg REST endpoint](https://aws.amazon.com/blogs/storage/query-amazon-s3-tables-from-open-source-trino-using-apache-iceberg-rest-endpoint) using its Iceberg connector with SigV4 authentication. Tell us which engine you are using when you open the request and we will point the catalog at it. If you are not using the Data Warehouse Connector, the [Bulk FHIR API](/docs/api/fhir/operations/bulk-fhir) exports resources as NDJSON that you can stage and load yourself. It runs on demand rather than on a schedule, and you own the loading and the incremental logic. ### See also - [Amazon Redshift](/docs/analytics/redshift) for the same pipeline read from Redshift - [Analytics](/docs/analytics) for program design, coding systems, and standard measures - [Bulk FHIR API](/docs/api/fhir/operations/bulk-fhir) - [Access Policies](/docs/access/access-policies) - [Server config: dataWarehouse](/docs/self-hosting/server-config#datawarehouse) --- ## Reset Password Endpoint import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ### POST `/auth/resetpassword` Initiates a password reset for a user. If successful, sends a password reset email to the user (unless `sendEmail` is set to false which is recommended for custom reset password flows). Then, the redirectUri in the email sent to the user will bring the user to a page that calls the [/auth/setpassword](/docs/api/auth/setpassword) endpoint after the user enters their new password. Check out [custom emails](/docs/user-management/custom-emails) for directions to create a custom reset password flow. :::info[] To see an example, check out the the code for the Medplum app's reset password page in [`ResetPasswordPage.tsx`](https://github.com/medplum/medplum/blob/main/packages/app/src/ResetPasswordPage.tsx). ::: :::warning[] Please note that you may need to specify _projectId_ if your User is project scoped and _recaptchaSiteKey_ and _recaptchaToken_ if you are using your own recaptcha keys. ::: #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | email | string | Yes | User's email address (3-72 characters) | | projectId | string | No | Project ID for project-scoped users. See [project scoped users](/docs/user-management/project-vs-server-scoped-users#project-scoped-users) Omit for system-level users | | sendEmail | boolean | No | Whether to send Medplum labeled reset email (defaults to true) | | redirectUri | string | No | URI to redirect after password reset | | recaptchaSiteKey | string | No | reCAPTCHA site key for verification | | recaptchaToken | string | No | reCAPTCHA token for verification | #### Response Returns a 200 OK response regardless of whether a user was found. #### Example ```typescript await medplum.post('auth/resetpassword', { email: 'user@example.com', projectId: 'project-123', sendEmail: true, redirectUri: 'https://app.example.com/reset', recaptchaSiteKey: '6LeIxAcTAAAAAJ55555555555555555555555555555555', recaptchaToken: 'recaptcha-token' }); ``` #### Notes - Reset links expire one hour after they are issued - Requesting a new reset invalidates any earlier reset link for that user - When resetting password for project-scoped users, `projectId` must be provided - Omit `projectId` when resetting password for system-level users - If you are building a custom reset password email, set sendEmail to false --- ## Set Password Endpoint ### POST `/auth/setpassword` :::note[] To see an example of the Medplum app's set password page, check out the code in [`SetPasswordPage.tsx`](https://github.com/medplum/medplum/blob/main/packages/app/src/SetPasswordPage.tsx). Notice that the page ::: Sets a new password for a user using a security request token. This endpoint is used to complete both password reset and user invitation flows. After redirect from the reset password or invite email, this endpoint can be called with the following parameters: #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | id | string | Yes | The UserSecurityRequest ID received from the reset password email | | secret | string | Yes | The security token received from the reset password email | | password | string | Yes | The new password (must be at least 8 characters) | #### Response - Returns `200 OK` if the password was successfully set - Returns `400 Bad Request` if: - The security request has already been used - The security request has expired - The secret is incorrect - The password is found in breach database - The password is less than 8 characters #### Link expiration Security requests are single use and time limited. Password reset links expire one hour after they are issued, and invitation links expire after seven days. Requesting a new password reset also invalidates any earlier reset link for that user, so only the most recent link works. #### Example ```typescript await medplum.post('auth/setpassword', { id: 'security-request-id', secret: 'security-token', password: 'new-password' }); ``` --- ## Address import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/address.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Address Type: An address expressed using postal conventions (as opposed to GPS or other location definition formats). This data type may be used to convey addresses for use in delivering mail as well as for visiting locations which might not be valid for mail delivery. There are a variety of postal address formats defined around the world. **Schema** ### Elements --- ## Age import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/age.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Age Type: A duration of time during which an organism (or a process) has existed. **Schema** ### Elements --- ## Annotation import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/annotation.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Annotation Type: A text note which also contains information about who made the statement and when. **Schema** ### Elements --- ## Attachment import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/attachment.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Attachment Type: For referring to data content defined in other formats. **Schema** ### Elements --- ## CodeableConcept import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/codeableconcept.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for CodeableConcept Type: A concept that may be defined by a formal reference to a terminology or ontology or may be provided by text. **Schema** ### Elements --- ## Coding import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/coding.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Coding Type: A reference to a code defined by a terminology system. **Schema** ### Elements --- ## ContactDetail import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/contactdetail.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for ContactDetail Type: Specifies contact information for a person or organization. **Schema** ### Elements --- ## ContactPoint import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/contactpoint.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for ContactPoint Type: Details for all kinds of technology mediated contact points for a person or organization, including telephone, email, etc. **Schema** ### Elements --- ## Contributor import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/contributor.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Contributor Type: A contributor to the content of a knowledge asset, including authors, editors, reviewers, and endorsers. **Schema** ### Elements --- ## Count import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/count.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Count Type: A measured amount (or an amount that can potentially be measured). Note that measured amounts include amounts that are not precisely quantified, including amounts involving arbitrary units and floating currencies. **Schema** ### Elements --- ## DataRequirement import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/datarequirement.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for DataRequirement Type: Describes a required data item for evaluation in terms of the type of data, and optional code or date-based filters of the data. **Schema** ### Elements --- ## Distance import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/distance.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Distance Type: A length - a value with a unit that is a physical distance. **Schema** ### Elements --- ## Dosage import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/dosage.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Dosage Type: Indicates how the medication is/was taken or should be taken by the patient. **Schema** ### Elements --- ## Duration import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/duration.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Duration Type: A length of time. **Schema** ### Elements --- ## ElementDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/elementdefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for ElementDefinition Type: Captures constraints on each element within the resource, profile, or extension. **Schema** ### Elements --- ## Expression import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/expression.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Expression Type: A expression that is evaluated in a specified context and returns a value. The context of use of the expression must specify the context in which the expression is evaluated, and how the result of the expression is used. **Schema** ### Elements --- ## Extension import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/extension.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Extension Type: Optional Extension Element - found in all resources. **Schema** ### Elements --- ## HumanName import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/humanname.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for HumanName Type: A human's name with the ability to identify parts and usage. **Schema** ### Elements --- ## Identifier import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/identifier.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Identifier Type: An identifier - identifies some entity uniquely and unambiguously. Typically this is used for business identifiers. **Schema** ### Elements --- ## MarketingStatus import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/marketingstatus.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for MarketingStatus Type: The marketing status describes the date when a medicinal product is actually put on the market or the date as of which it is no longer available. **Schema** ### Elements --- ## Meta import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/meta.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Meta Type: The metadata about a resource. This is content in the resource that is maintained by the infrastructure. Changes to the content might not always be associated with version changes to the resource. **Schema** ### Elements --- ## Money import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/money.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Money Type: An amount of economic utility in some recognized currency. **Schema** ### Elements --- ## MoneyQuantity import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/moneyquantity.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; An amount of money. With regard to precision, see [Decimal Precision](https://www.hl7.org/fhir/datatypes.html#precision) **Schema** ### Elements --- ## Narrative import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/narrative.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Narrative Type: A human-readable summary of the resource conveying the essential clinical and business information for the resource. **Schema** ### Elements --- ## ParameterDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/parameterdefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for ParameterDefinition Type: The parameters to the module. This collection specifies both the input and output parameters. Input parameters are provided by the caller as part of the $evaluate operation. Output parameters are included in the GuidanceResponse. **Schema** ### Elements --- ## Period import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/period.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Period Type: A time period defined by a start and end date and optionally time. **Schema** ### Elements --- ## Population import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/population.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Population Type: A populatioof people with some set of grouping criteria. **Schema** ### Elements --- ## ProdCharacteristic import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/prodcharacteristic.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for ProdCharacteristic Type: The marketing status describes the date when a medicinal product is actually put on the market or the date as of which it is no longer available. **Schema** ### Elements --- ## ProductShelfLife import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/productshelflife.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for ProductShelfLife Type: The shelf-life and storage information for a medicinal product item or container can be described using this class. **Schema** ### Elements --- ## Quantity import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/quantity.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Quantity Type: A measured amount (or an amount that can potentially be measured). Note that measured amounts include amounts that are not precisely quantified, including amounts involving arbitrary units and floating currencies. **Schema** ### Elements --- ## Range import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/range.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Range Type: A set of ordered Quantities defined by a low and high limit. **Schema** ### Elements --- ## Ratio import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/ratio.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Ratio Type: A relationship of two Quantity values - expressed as a numerator and a denominator. **Schema** ### Elements --- ## Reference import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/reference.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Reference Type: A reference from one resource to another. **Schema** ### Elements --- ## RelatedArtifact import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/relatedartifact.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for RelatedArtifact Type: Related artifacts such as additional documentation, justification, or bibliographic references. **Schema** ### Elements --- ## SampledData import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/sampleddata.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for SampledData Type: A series of measurements taken by a device, with upper and lower limits. There may be more than one dimension in the data. **Schema** ### Elements --- ## Signature import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/signature.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Signature Type: A signature along with supporting context. The signature may be a digital signature that is cryptographic in nature, or some other signature acceptable to the domain. This other signature may be as simple as a graphical image representing a hand-written signature, or a signature ceremony Different signature approaches have different utilities. **Schema** ### Elements --- ## SimpleQuantity import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/simplequantity.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A fixed quantity (no comparator) **Schema** ### Elements --- ## SubstanceAmount import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/substanceamount.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for SubstanceAmount Type: Chemical substances are a single substance type whose primary defining element is the molecular structure. Chemical substances shall be defined on the basis of their complete covalent molecular structure; the presence of a salt (counter-ion) and/or solvates (water, alcohols) is also captured. Purity, grade, physical form or particle size are not taken into account in the definition of a chemical substance or in the assignment of a Substance ID. **Schema** ### Elements --- ## Timing import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/timing.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for Timing Type: Specifies an event that may occur multiple times. Timing schedules are used to record when things are planned, expected or requested to occur. The most common usage is in dosage instructions for medications. They are also used when planning care of various kinds, and may be used for reporting the schedule to which past regular activities were carried out. **Schema** ### Elements --- ## TriggerDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/triggerdefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for TriggerDefinition Type: A description of a triggering event. Triggering events can be named events, data events, or periodic, as determined by the type element. **Schema** ### Elements --- ## UsageContext import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/datatypeDefinitions/usagecontext.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Base StructureDefinition for UsageContext Type: Specifies clinical/business/etc. metadata that can be used to retrieve, index and/or categorize an artifact. This metadata can either be specific to the applicable population (e.g., age category, DRG) or the specific context of care (e.g., venue, care setting, provider of care). **Schema** ### Elements --- ## AccessPolicy import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/accesspolicy.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Access Policy for user or user group that defines how entities can or cannot access resources. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## Agent import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/agent.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Configuration details for an instance of the Medplum agent application. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## AsyncJob import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/asyncjob.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Contains details of long running asynchronous/background jobs. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## Bot import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/bot.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Bot account for automated actions. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## BulkDataExport import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/bulkdataexport.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; User specific configuration for the Medplum application. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## ClientApplication import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/clientapplication.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Medplum client application for automated access. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## Cron import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/cron.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A scheduled job that executes a Bot on a recurring schedule. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## DicomInstance import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/dicominstance.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Definition of a DICOM instance, which represents a single DICOM image and related metadata that are part of a DICOM series. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## DicomSeries import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/dicomseries.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Definition of a DICOM series, which represents a collection of DICOM images and related metadata that are part of a DICOM study. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## DicomStudy import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/dicomstudy.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Definition of a DICOM study, which represents a collection of DICOM images and related metadata. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## DomainConfiguration import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/domainconfiguration.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Domain specific configuration for the Medplum application. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## Enterprise import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/enterprise.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A group of Medplum projects managed as a single enterprise. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## IdentityProvider import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/identityprovider.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; External Identity Provider (IdP) configuration details. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## JsonWebKey import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/jsonwebkey.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A JSON object that represents a cryptographic key. The members of the object represent properties of the key, including its value. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## Login import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/login.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Login event and session details. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## Package import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/package.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Definition of a marketplace package, which represents a set of automated actions that can be taken by a system, such as a subscription or a workflow. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## PackageInstallation import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/packageinstallation.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Definition of a marketplace package installation, which represents the installation of a specific package release in a system. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## PackageRelease import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/packagerelease.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Definition of a marketplace package release, which represents a specific release of a package that can be installed and used by a system. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## Project import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/project.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Encapsulation of resources for a specific project or organization. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## ProjectMembership import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/projectmembership.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Medplum project membership. A project membership grants a user access to a project. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## SmartAppLaunch import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/smartapplaunch.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; This resource contains context details for a SMART App Launch. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## User import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/user.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Representation of a human user of the system. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## UserConfiguration import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/userconfiguration.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; User specific configuration for the Medplum application. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## UserSecurityRequest import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/medplumDefinitions/usersecurityrequest.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; User security request for the 'forgot password' flow, email verification, etc. **Schema** ### Elements ### Search Parameters ### Inherited Elements --- ## Parameters $ai The `$ai` operation provides an interface for calling AI language models (like OpenAI's GPT) through Medplum. This operation supports both standard request/response and streaming modes. ### Use Cases - **Clinical Decision Support**: Get AI assistance for clinical questions - **Documentation Assistance**: Help generate clinical notes and summaries - **Patient Education**: Create patient-friendly explanations of medical concepts - **Data Analysis**: Analyze and summarize patient data with AI assistance ### Prerequisites The AI feature must be enabled for your project. Contact your Medplum administrator or ensure `ai` is included in your project's features list. The operation is intended for practitioners, bots and client applications. For patient-facing AI features, call `$ai` from a [Bot](/docs/bots). By default the operation calls OpenAI at `https://api.openai.com/v1`. To route requests through a [LiteLLM](https://docs.litellm.ai/) proxy or other OpenAI-compatible endpoint, set the optional project secret `LLM_BASE_URL` (e.g. `https://litellm.example.com/v1`); the `OPENAI_API_KEY` secret is then used as the proxy's bearer token. See [`$ai` Operation](/docs/ai/ai-operation) for details. ### Invoke the `$ai` operation ``` [base]/Parameters/$ai ``` For example: ```bash curl -X POST 'https://api.medplum.com/fhir/R4/Parameters/$ai' \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "messages", "valueString": "[{\"role\": \"user\", \"content\": \"What is FHIR?\"}]" }, { "name": "model", "valueString": "gpt-4" } ] }' ``` ### Parameters | Name | Type | Description | Required | | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------- | -------- | | `messages` | `string` | JSON string containing the conversation messages array | Yes | | `model` | `string` | OpenAI model to use (e.g., `gpt-4`, `gpt-3.5-turbo`) | Yes | | `tools` | `string` | JSON string containing the tools array for function calling | No | | `reasoning_effort` | `string` | Reasoning effort for reasoning models: `none`, `minimal`, `low`, `medium`, `high` or `xhigh` | No | | `api` | `code` | OpenAI endpoint: `chat` or `responses`. Defaults to `chat`; `tools` with a `reasoning_effort` other than `none` use `responses` | No | #### Messages Format The `messages` parameter should be a JSON-encoded array of message objects following the OpenAI chat format: ```json [ { "role": "system", "content": "You are a helpful healthcare assistant." }, { "role": "user", "content": "What is the normal range for blood pressure?" } ] ``` #### Tools Format (Function Calling) The `tools` parameter enables function calling capabilities: ```json [ { "type": "function", "function": { "name": "get_patient_vitals", "description": "Retrieve patient vital signs", "parameters": { "type": "object", "properties": { "patient_id": { "type": "string", "description": "The patient's ID" } }, "required": ["patient_id"] } } } ] ``` ### Output | Name | Type | Description | | ------------ | -------- | --------------------------------------- | | `content` | `string` | The AI response content | | `tool_calls` | `string` | JSON string containing tool calls array | #### Example Response ```json { "resourceType": "Parameters", "parameter": [ { "name": "content", "valueString": "FHIR (Fast Healthcare Interoperability Resources) is a standard for exchanging healthcare information electronically..." } ] } ``` #### Response with Tool Calls ```json { "resourceType": "Parameters", "parameter": [ { "name": "tool_calls", "valueString": "[{\"id\": \"call_abc123\", \"type\": \"function\", \"function\": {\"name\": \"get_patient_vitals\", \"arguments\": {\"patient_id\": \"patient-123\"}}}]" } ] } ``` ### Streaming Mode The operation supports Server-Sent Events (SSE) streaming for real-time responses. To enable streaming, set the `Accept` header to `text/event-stream`: ```bash curl -X POST 'https://api.medplum.com/fhir/R4/Parameters/$ai' \ -H "Content-Type: application/fhir+json" \ -H "Accept: text/event-stream" \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -d '...' ``` Streaming responses arrive as SSE events: ``` data: {"content": "FHIR "} data: {"content": "(Fast "} data: {"content": "Healthcare "} data: {"content": "Interoperability "} data: {"content": "Resources) "} data: [DONE] ``` :::note[] Tool calls are not supported in streaming mode. ::: ### Error Responses #### Feature Not Enabled ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "forbidden", "details": { "text": "Forbidden" } } ] } ``` #### Invalid Messages Format ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "Messages must be an array" } } ] } ``` ### Security Considerations - The API key is sent with each request - ensure you're using HTTPS - Consider using server-side bots to proxy AI calls and protect your API key - Review AI responses before using them in clinical settings ### Related Documentation - [Medplum AI Documentation](/docs/ai) - Overview of AI capabilities in Medplum - [Bots](/docs/bots) - Create server-side automation with AI capabilities --- ## AsyncJob $cancel The `$cancel` operation allows you to cancel a running asynchronous job. This is useful when you need to stop a long-running operation that is no longer needed. ### Use Cases - **Resource Management**: Cancel jobs that are no longer needed to free up server resources - **Error Recovery**: Stop a job that was started with incorrect parameters - **User Control**: Allow users to cancel long-running exports or operations they initiated ### Invoke the `$cancel` operation ``` [base]/AsyncJob/[id]/$cancel ``` For example: ```bash curl -X POST 'https://api.medplum.com/fhir/R4/AsyncJob/job-id/$cancel' \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer MY_ACCESS_TOKEN" ``` ### Parameters This operation has no input parameters. The job ID is specified in the URL path. ### Output The operation returns an OperationOutcome indicating success or failure. #### Successful Response ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "information", "code": "informational", "details": { "text": "All OK" } } ] } ``` ### Behavior The operation behavior depends on the current status of the AsyncJob: | Current Status | Behavior | | -------------- | -------------------------------------------------- | | `accepted` | Job is cancelled, status changes to `cancelled` | | `cancelled` | No action taken (already cancelled) | | Other | Error returned - job cannot be cancelled | #### Status Transitions ``` accepted → cancelled (via $cancel) ``` :::note[] Only jobs with `accepted` status can be cancelled. Jobs that are already `completed`, `failed`, or in other terminal states cannot be cancelled. ::: ### Error Responses #### Job Already Completed If the job has already completed or is in a non-cancellable state: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "AsyncJob cannot be cancelled if status is not 'accepted', job had status 'completed'" } } ] } ``` #### Job Not Found If the AsyncJob doesn't exist or you don't have access: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "not-found", "details": { "text": "Not found" } } ] } ``` ### AsyncJob Lifecycle AsyncJobs are created by various long-running operations: 1. **Bulk FHIR Export** - `$export` operations 2. **Project Expunge** - `$expunge` with `everything=true` 3. **Reindex Operations** - `$reindex` operations 4. **Asynchronous FHIR batches** - `POST` to the FHIR base with `Prefer: respond-async` (see [Processing asynchronous bundles](/docs/fhir-datastore/processing-async-bundles)) 5. **Other Async Operations** - Various operations that run asynchronously #### Example: Cancel a Bulk Export ```bash # Start a bulk export curl -X GET 'https://api.medplum.com/fhir/R4/$export' \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -H "Prefer: respond-async" # Response includes Content-Location header # Content-Location: https://api.medplum.com/fhir/R4/AsyncJob/abc123 # Cancel the export if needed curl -X POST 'https://api.medplum.com/fhir/R4/AsyncJob/abc123/$cancel' \ -H "Authorization: Bearer MY_ACCESS_TOKEN" ``` ### Related Documentation - [Bulk FHIR Export](/docs/api/fhir/operations/bulk-fhir) - Operations that create AsyncJobs - [Resource $expunge](/docs/api/fhir/operations/expunge) - Expunge operations that may run async --- ## Binary $presigned-url Binary resources containing non-FHIR data such as images or PDF documents are often used to enrich and add context to clinical resources. If an unauthenticated user needs to read or write those files, this can be allowed by generating a pre-signed URL for the Binary. Any user can access this temporary secure URL to either read the contents of the file, or to write new data to the Binary. ### Invocation ``` GET [base]/Binary/[id]/$presigned-url ``` ### Input Query Parameters | Parameter | Cardinality | Type | Description | | --------- | ----------- | --------- | ------------------------------------------------------------------------------- | | `upload` | 0..1 | `boolean` | Whether the link is used for reading (default = false) or writing (set to true) | ### Output Parameters | Parameter | Cardinality | Type | Description | | --------- | ----------- | -------- | ---------------------------------------------------------------------- | | `url` | 1..1 | `string` | The presigned URL granting read or write access to the Binary contents | ### Example #### Request **Read URL:** ```http GET /fhir/R4/Binary/[id]/$presigned-url ``` **Write URL:** ```http GET /fhir/R4/Binary/[id]/$presigned-url?upload=true ``` #### Response ```json { "resourceType": "Parameters", "parameter": [ { "name": "url", "valueUri": "https://storage.medplum.com/binary/c138a343-02e1-40f0-801f-e3d7f5317d4e/f4609304-c1ca-4a5b-af7d-77a289a2ac55?Signature=RJKaFn[...]" } ] } ``` #### Using the Presigned URL :::warning[] The generated presigned URLs are **unauthenticated**, and can be accessed by anyone who has the URL without an access token. Care should be taken to ensure that the URLs are restricted to the intended users. ::: For reads/downloads, the URL can be accessed directly to retrieve the Binary contents: ```bash curl 'https://storage.medplum.com/binary/c138a343-02e1-40f0-801f-e3d7f5317d4e/f4609304-c1ca-4a5b-af7d-77a289a2ac55?Signature=RJKaFn[...]' ``` To send data to a presigned upload URL, use an HTTP PUT request with the same content type as defined in the resource's `Binary.contentType` field: ```bash curl -X PUT 'https://storage.medplum.com/binary/c138a343-02e1-40f0-801f-e3d7f5317d4e/f4609304-c1ca-4a5b-af7d-77a289a2ac55?Signature=RJKaFn[...]' \ -H 'Content-Type: text/plain' --data-raw 'lorem ipsum dolor sit amet...' ``` --- ## Binary $scan Checks the contents of a Binary for malware using [Amazon GuardDuty Malware Protection for S3](https://docs.aws.amazon.com/guardduty/latest/ug/gdu-malware-protection-s3.html). `$scan` returns the Binary's scan result if it has one. Otherwise it asks GuardDuty for an on-demand scan and waits for the result, which usually takes seconds. The operation doesn't rely on GuardDuty scanning uploads automatically, and works with automatic scanning turned off. `$scan` waits up to 30 seconds. If the result isn't ready by then, it returns `SCAN_REQUESTED`, and calling it again waits for the same scan. With `Prefer: respond-async`, `$scan` returns `202 Accepted` immediately, with a `Content-Location` header pointing at an `AsyncJob`. The job waits up to 15 minutes, then completes with the result in its output `Parameters` as the `return` parameter. A `THREATS_FOUND` result still completes the job; the job fails only if the scan can't be requested. GuardDuty bills every on-demand scan, so `$scan` avoids sending repeat scans: - A Binary with a final result (`NO_THREATS_FOUND`, `THREATS_FOUND` or `UNSUPPORTED`) is never scanned again. - When `$scan` sends a scan, it tags the S3 object `MedplumMalwareScanRequested` with the request time. Until a result arrives, later calls wait for that scan instead of sending another one. If no result arrives within an hour, the next call sends a new scan. Two calls made at the same moment can still both send a scan. - A `FAILED` or `ACCESS_DENIED` result is returned, and retried on the next call. ### Invocation ``` POST [base]/Binary/[id]/$scan ``` Add the `Prefer: respond-async` header to run the scan as an `AsyncJob`. Any user with read access to the Binary can call this operation. ### Output The operation returns an `OperationOutcome` with a single issue. `issue.details.coding` uses the system `https://medplum.com/fhir/CodeSystem/malware-scan-status`. | Code | Severity | Issue code | Meaning | | ------------------ | ------------- | --------------- | ------------------------------------------------------------------------------------------------ | | `NO_THREATS_FOUND` | `information` | `informational` | The scan finished and found no threats | | `THREATS_FOUND` | `error` | `security` | The scan finished and found a potential threat | | `UNSUPPORTED` | `warning` | `not-supported` | GuardDuty cannot scan this object, for example a password-protected archive or an oversized file | | `FAILED` | `error` | `exception` | GuardDuty couldn't finish the scan. The next call sends a new scan | | `ACCESS_DENIED` | `error` | `forbidden` | GuardDuty couldn't read the object. The next call sends a new scan | | `SCAN_REQUESTED` | `information` | `informational` | The scan is still in progress. Call `$scan` again for the result | When the result comes from a scan that `$scan` requested, the issue has a `https://medplum.com/fhir/StructureDefinition/malware-scan-requested` extension whose `valueDateTime` is when the scan was requested. Results from automatic scans of new uploads don't have it. GuardDuty doesn't report when a scan of an S3 object finishes, so no completion time is available. ### Example #### Request ```http POST /fhir/R4/Binary/[id]/$scan ``` #### Response ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "information", "code": "informational", "extension": [ { "url": "https://medplum.com/fhir/StructureDefinition/malware-scan-requested", "valueDateTime": "2026-09-25T16:44:55.296Z" } ], "details": { "coding": [{ "system": "https://medplum.com/fhir/CodeSystem/malware-scan-status", "code": "NO_THREATS_FOUND" }], "text": "No threats found" } } ] } ``` ### Requirements - Binary storage must be S3 (`binaryStorage: "s3:"`). - The storage bucket needs a GuardDuty Malware Protection plan with tagging enabled. The `guardDutyMalwareProtectionEnabled` infra config option creates one. It also grants the server `s3:GetObjectTagging`, `s3:PutObjectTagging` and `guardduty:SendObjectMalwareScan`. - SSE-C encryption (`sseCustomerKey`) is not supported, because GuardDuty cannot read SSE-C objects. ### Serving scanned Binaries GuardDuty Malware Protection supports two setups: - **Scan every upload** (`guardDutyMalwareProtectionEnabled`): GuardDuty scans each new object, and CloudFront only serves Binaries tagged `NO_THREATS_FOUND`. Binaries that haven't been scanned yet, or that returned any other result, are not served. - **Scan on demand** (`guardDutyMalwareProtectionEnabled` and `guardDutyMalwareProtectionOnDemandOnly`): for deployments with many existing, unscanned Binaries. Only `$scan` sends scans, and CloudFront blocks only Binaries tagged `THREATS_FOUND`. Unscanned Binaries and Binaries with a scan in progress are still served. GuardDuty has no on-demand-only mode, so this setup limits automatic scanning to the `guardduty-on-demand-only/` prefix, which Medplum never writes to. On-demand scans ignore the prefix. When the storage bucket and CloudFront distribution are in different regions, apply the read gate with `medplum aws update-bucket-policies`. Pass `--guardduty-malware-protection`, and add `--guardduty-on-demand-only` for on-demand scanning. :::note The gate only applies when CloudFront reads from the bucket. CloudFront can cache a Binary for up to a day, so a Binary served before GuardDuty tags it `THREATS_FOUND` may stay available from the cache until that entry expires. ::: --- ## Bot $deploy import ExampleCode from '!!raw-loader!@site/..//examples/src/api/fhir/operations/bot-deploy.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; The `$deploy` operation uploads and activates Bot code on the Medplum server. This is essential for making your automation logic available for execution by subscriptions, manual invocation, or scheduled tasks. Deploying bots allows you to version and update your server-side automation without downtime, making it a critical operation for maintaining healthcare workflows in production environments. ### Use Cases - **CI/CD Pipelines**: Automatically deploy bot updates as part of your continuous deployment workflow - **Development Iteration**: Quickly test code changes by deploying updated bot logic during development - **Production Updates**: Roll out new features or bug fixes to production bots with minimal disruption - **Multi-Environment Management**: Deploy different bot versions across staging and production environments :::tip[] If you're not familiar with Medplum Bots, you may want to review the [Bot Basics documentation](/docs/bots/bot-basics) first. ::: ### Invoke `$deploy` Operation To use the `$deploy` operation, you will need to make a `POST` request with the Bot's `id`. Additionally, the `$deploy` operation takes two body parameters: 1. `filename`: The name of the file the bot is stored in. If left blank, it will default to `index.js`. 2. `code`: The bot's code that will be executed when it is run. **Typescript** {ExampleCode} **CLI** {ExampleCode} **cURL** {ExampleCode} ### Related - [Bot $execute](/docs/api/fhir/operations/bot-execute) - Execute a deployed bot - [Bots In Production](/docs/bots/bots-in-production#deploying-your-bot) - Deployment best practices and CI/CD setup - [Bot Basics](/docs/bots/bot-basics) - Introduction to Medplum Bots - [Medplum CLI](/docs/cli) - Command-line tool for bot deployment - [FHIR Bot Resource](https://www.medplum.com/docs/api/fhir/medplum/bot) - Medplum Bot resource reference --- ## Bot $execute import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import ExampleCode from '!!raw-loader!@site/../examples/src/api/fhir/operations/bot-execute.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; The `$execute` operation runs a Medplum Bot on-demand, allowing you to trigger server-side automation logic via API calls. Bots can process incoming data, integrate with external systems, and return custom responses-making this operation the primary way to leverage Medplum's automation capabilities. Whether you're processing HL7v2 messages from legacy systems, responding to webhooks, or building custom API endpoints, `$execute` provides the flexibility to handle virtually any healthcare integration scenario. ### Use Cases - **Webhook Handling**: Receive and process incoming webhooks from external services (labs, pharmacies, payers) - **HL7v2 Integration**: Parse and transform HL7v2 messages from legacy healthcare systems - **Custom API Endpoints**: Build application-specific endpoints that return custom JSON responses - **On-Demand Processing**: Trigger data transformations, validations, or calculations programmatically - **External System Integration**: Connect to third-party APIs and synchronize data with your FHIR resources :::tip[] If you're not familiar with Medplum Bots, you may want to review [Bot Basics](/docs/bots/bot-basics) documentation first. ::: ### Invoke a bot by ID Invoke a bot by ID when you know the Bot's ID in advance. #### Finding your Bot Id You can find the `id` of your Bot by clicking on the **Details** tab of the Bot resource. In this example, it is ` 43ac3060-ff20-49e8-9682-bf91ab3a5191` ![Find your Bot ID](/img/app/bots/find_bot_id.png) #### Using POST ``` POST [base]/Bot/[id]/$execute ``` ##### Examples **TypeScript** The [MedplumClient](/docs/sdk/core.medplumclient) TypeScript class provides a `executeBot` convenience method {ExampleCode} **CLI** ```bash medplum login medplum post 'Bot/[id]/$execute' '{ "foo": "bar" }' ``` **cURL** ```bash curl 'https://api.medplum.com/fhir/R4/Bot/[id]/$execute' \ -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $MY_ACCESS_TOKEN" \ -d '{"foo":"bar"}' ``` #### Using GET Query parameters will be passed to the bot as type `Record` (see [Content Types](#content-types) below) ``` GET [base]/Bot/[id]/$execute?params ``` **TypeScript** {ExampleCode} **CLI** ```bash medplum login medplum get 'Bot/[id]/$execute?foo=bar' ``` **cURL** ```bash curl 'https://api.medplum.com/fhir/R4/Bot/[id]/$execute?foo=bar' \ -H "Authorization: Bearer $MY_ACCESS_TOKEN" ``` ### Invoke a bot by identifier Sometimes you may not know the Medplum Bot ID in advance. In that case, you can invoke a Bot by `Identifier`. This is also useful when the same conceptual bot exists in multiple Medplum projects. Each bot will have a different ID, but they can all have the same identifier. #### Using POST ``` POST [base]/Bot/$execute?identifier=[system]|[code] ``` ##### Examples **TypeScript** The [MedplumClient](/docs/sdk/core.medplumclient) `executeBot` convenience method supports both `id: string` and `identifier: Identifier`: {ExampleCode} **CLI** ```bash medplum login medplum post 'Bot/[id]/$execute?identifier=https://example.com/bots|1234' '{ "foo": "bar" }' ``` **cURL** ```bash curl 'https://api.medplum.com/fhir/R4/Bot/$execute?identifier=https://example.com/bots|1234' \ -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $MY_ACCESS_TOKEN" \ -d '{"foo":"bar"}' ``` #### Using GET Query parameters will be passed to the bot as type `Record` (see [Content Types](#content-types) below) ``` GET [base]/Bot/$execute?identifier=[system]|[code]¶ms ``` **TypeScript** {ExampleCode} **CLI** ```bash medplum login medplum get 'Bot/$execute?identifier=https://example.com/bots|1234' ``` **cURL** ```bash curl 'https://api.medplum.com/fhir/R4/Bot/$execute?identifier=https://example.com/bots|1234' \ -H "Authorization: Bearer $MY_ACCESS_TOKEN" ``` ### Content Types Medplum Bots support a variety of input content types. Specify the input content type using the standard `Content-Type` HTTP header, or as an optional parameter to `MedplumClient.executeBot()`. | Content-Type | typeof `event.input` | Description | | ------------------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `text/plain` | `string` | `` is parsed as plaintext string | | `application/json` | `Record` | `` is parsed as JSON-encoded object | | `application/x-www-form-urlencoded ` | `Record` | `` is parsed as URL-encoded string, resulting in a key/value map | | `application/fhir+json` | [`Resource`](/docs/api/fhir/resources) | `` is parsed as a [FHIR Resource](/docs/fhir-basics#storing-data-resources) encoded as JSON | | `x-application/hl7-v2+er7` | [`HL7Message`](/docs/sdk/core.hl7message) | `` is a string that should be parsed as a pipe-delimited HL7v2 message. HL7v2 is a common text-based message protocol used in legacy healthcare systems | The input data that will be parsed according to `CONTENT_TYPE` and passed into your Bot as `event.input`. ### Asynchronous Execution To run bots asynchronously, you can specify the `Prefer: respond-async` header to move execution of the bot to the background. Asynchronous execution will result in an HTTP `202 Accepted` response immediately, and the bot will continue running in the background. #### Making an Async Request Add the `Prefer: respond-async` header to any bot execution request: **TypeScript** {ExampleCode} **CLI** ```bash medplum post 'Bot/[id]/$execute' '{ "foo": "bar" }' --prefer-async ``` **cURL** ```bash curl 'https://api.medplum.com/fhir/R4/Bot/[id]/$execute' \ -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $MY_ACCESS_TOKEN" \ -H "Prefer: respond-async" \ -d '{"foo":"bar"}' ``` #### Checking Job Status When using async execution, the server returns a `202 Accepted` response with a `Content-Location` header containing the URL to check the job status: ```http HTTP/1.1 202 Accepted Content-Location: https://api.medplum.com/fhir/R4/job/[job-id]/status ``` You can poll this URL to check the status of your bot execution. In general, for long running jobs it's best to poll no more frequently than 1 second. **TypeScript** The `MedplumClient` provides helper utilities to poll job status automatically {ExampleCode} However, you can also manually poll the status URL {ExampleCode} **CLI** ```bash # Check job status medplum get 'job/[job-id]/status' ``` **cURL** ```bash # Check job status curl 'https://api.medplum.com/fhir/R4/job/[job-id]/status' \ -H "Authorization: Bearer $MY_ACCESS_TOKEN" ``` #### AsyncJob Resource Structure The status endpoint returns an `AsyncJob` resource with the following key fields: - **`status`**: Current job status - `accepted` - Job has been queued - `active` - Job is currently running - `completed` - Job finished successfully - `error` - Job failed - `cancelled` - Job was cancelled - **`output`**: Contains the results when status is `completed` or `error` - For successful bot execution: `parameter[0].name` will be `responseBody` - For failed execution: `parameter[0].name` will be `outcome` with error details #### Example Response When the bot execution completes successfully: {ExampleCode} When the bot execution fails: {ExampleCode} ### Related - [Bot $deploy](/docs/api/fhir/operations/bot-deploy) - Deploy bot code before execution - [Bot Basics](/docs/bots/bot-basics) - Introduction to Medplum Bots - [Bots In Production](/docs/bots/bots-in-production) - Best practices for production bot deployments - [HL7 Interfacing](/docs/integration/hl7-interfacing) - Processing HL7v2 messages with bots - [Consuming Webhooks](/docs/bots/consuming-webhooks) - Handle incoming webhooks with bots --- ## Bulk FHIR API import ExampleCode from '!!raw-loader!@site/../examples/src/api/fhir/operations/bulk-fhir.py'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; Medplum supports the [Bulk FHIR API 2.0.0](https://hl7.org/fhir/uv/bulkdata/STU2/). The Bulk FHIR API uses [Backend Services Authorization](https://www.hl7.org/fhir/smart-app-launch/backend-services.html). ### Use Cases - **Population Health Reporting**: Export data for entire patient populations for analytics and reporting dashboards - **Data Migration**: Transfer large datasets between FHIR-compliant systems during system migrations - **Regulatory Compliance**: Meet ONC, MIPS, and other regulatory requirements for bulk data access - **Research Data Extraction**: Export de-identified or consented patient data for clinical research studies - **Data Warehousing**: Populate analytics data warehouses with comprehensive FHIR data exports The premise of the Bulk FHIR API is that it allows you to create a **bulk export of data for multiple patients**. There are different ways to export data: - From a Group of patients, which will export everything in each patient's compartment - As a system level export of all FHIR resources in a Project The export process is asynchronous, and you will need to poll a status URL returned when you start the export. After the [BulkDataExport resource](/docs/api/fhir/medplum/bulkdataexport) with the export results is available, it will contain a set of URLs where you can download the exported data in [NDJSON][ndjson] format. [ndjson]: https://jsonlines.org/ ### Access Policy Requirements Because the bulk export process is asynchronous, your [AccessPolicy](/docs/access/access-policies) must grant you access to the `AsyncJob` resourceType. This is required to poll the status of the export operation. Without access to `AsyncJob`, you will not be able to check the status of your export or retrieve the results. Your AccessPolicy should include an entry like this: ```json { "resourceType": "AccessPolicy", "resource": [ { "resourceType": "AsyncJob", "readonly": true } ] } ``` ### Group Export To specify which patients need to be included in the export, construct a [Group](/docs/api/fhir/resources/group) resource and add specific patients as `Group.member.entity.` To start the process of exporting the resources, make an HTTP `GET` request for `/fhir/R4/Group//$export?_outputFormat=ndjson`. This initiates a Bulk Data Export transaction and return links to download URLs for requested resources. ```bash curl 'https://api.medplum.com/fhir/R4/Group//$export?_outputFormat=ndjson' \ -H 'Authorization: Bearer ' ``` | Resource in Medplum App | Usage in Bulk FHIR | | -------------------------------------- | ------------------------------------------------------------------------ | | [Group](https://app.medplum.com/Group) | All patients you want to include must be included as Group.member.entity | ### System Level Export An export can also be performed for all resources in a Project by making a `GET` request for `/fhir/R4/$export`. {ExampleCode} ### Related Reading - [Reporting and Analytics](/docs/analytics) overview - [ONC Certification](/docs/compliance/onc) compliance docs - [Standardized API for patient and population services](https://www.healthit.gov/test-method/standardized-api-patient-and-population-services) on HealthIT.gov --- ## Patient $ccda-export The `$ccda-export` operation generates a Consolidated Clinical Document Architecture (C-CDA) document for a patient. C-CDA is the industry-standard XML format for exchanging clinical summaries between healthcare systems, making this operation essential for interoperability and regulatory compliance. This operation produces a complete or time-windowed patient history in a format that can be consumed by EHRs, health information exchanges (HIEs), and patient portals-enabling seamless care coordination across organizational boundaries. Internally, `$ccda-export` builds an IPS-style patient summary ([`$summary`](./patient-summary)), then converts that FHIR Composition bundle to C-CDA XML via `@medplum/ccda`. ### Use Cases - **Health Information Exchange**: Share patient records with other healthcare organizations via HIEs - **Care Transitions**: Generate discharge summaries or transfer documents when patients move between care settings - **Patient Portal Downloads**: Allow patients to download their medical records in a standard format - **ONC Certification Compliance**: Meet certification requirements for patient data export capabilities - **Referral Documentation**: Provide comprehensive clinical summaries when referring patients to specialists ### Try it in the Medplum App You can exercise this operation from the patient **Export** tab without calling the API directly. Open `https://app.medplum.com/Patient//export` and use the export format controls to choose **C-CDA** (CCD) or **C-CDA Referral**, optionally set author and date filters, then download the resulting XML. ### Invocation ``` GET [base]/Patient/[id]/$ccda-export POST [base]/Patient/[id]/$ccda-export ``` ```bash curl 'https://api.medplum.com/fhir/R4/Patient//$ccda-export' \ -H 'Authorization: Bearer ' ``` ### Input Parameters `$ccda-export` accepts the same clinical filtering and authorship parameters as [`$summary`](./patient-summary), plus a C-CDA-specific `type` parameter that selects the document-level template. | Parameter | Cardinality | Type | Description | |-----------|-------------|------|-------------| | `type` | 0..1 | `code` | C-CDA document type. Use `referral` to generate a Referral Note. When omitted (or any other value), Medplum generates a Continuity of Care Document (CCD) / Summarization of Episode Note. | | `author` | 0..1 | `Reference` | Reference to Practitioner, PractitionerRole, or Organization to list as the composition author. Defaults to the current user. | | `authoredOn` | 0..1 | `instant` | The date/time the summary was authored. Defaults to current time. | | `start` | 0..1 | `date` | Start date for filtering resources by care date. If omitted, all records prior to `end` are in scope. | | `end` | 0..1 | `date` | End date for filtering resources by care date. If omitted, all records after `start` are in scope. | | `_since` | 0..1 | `instant` | Only include resources modified since this date | | `identifier` | 0..1 | `string` | Identifier for the composition (accepted; reserved for IPS alignment) | | `profile` | 0..1 | `canonical` | Profile to validate against (accepted; reserved for IPS alignment) | #### Document types (`type`) | `type` value | Document | LOINC | Notes | |--------------|----------|-------|-------| | *(omitted)* | Continuity of Care Document (CCD) | `34133-9` Summarization of Episode Note | Default export used by the Medplum App **C-CDA** option | | `referral` | Referral Note | `57133-1` Referral Note | Used by the Medplum App **C-CDA Referral** option. Sets Referral Note document template IDs and uses Reason for Referral / `ServiceRequest` content when present | #### Example: Referral Note ```bash curl -X POST 'https://api.medplum.com/fhir/R4/Patient//$ccda-export' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/fhir+json' \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "type", "valueCode": "referral" }, { "name": "start", "valueDate": "2024-01-01" }, { "name": "end", "valueDate": "2024-12-31" }, { "name": "author", "valueReference": { "reference": "Practitioner/example" } } ] }' ``` You can also pass parameters as query string values on GET, for example: ```bash curl 'https://api.medplum.com/fhir/R4/Patient//$ccda-export?type=referral' \ -H 'Authorization: Bearer ' ``` ### Output Returns C-CDA XML (`application/cda+xml`) as a raw Binary response. ### Behavior 1. **Data collection**: Uses [`$summary`](./patient-summary) (which gathers patient data via [`$everything`](./patient-everything)) to build an IPS Composition bundle 2. **Document template**: Applies CCD or Referral Note document-level template IDs and LOINC document type codes based on `type` 3. **XML serialization**: Converts the FHIR bundle to C-CDA with `@medplum/ccda`, then returns the XML ### Related - [C-CDA Integration Guide](/docs/integration/c-cda) - Detailed guide on working with C-CDA documents - [Patient $summary](./patient-summary) - IPS summary used as the FHIR source for export - [Patient $everything](./patient-everything) - Export all patient data in FHIR format - [ONC Certification](/docs/compliance/onc) - Compliance requirements for patient data export - [FHIR Patient Resource](https://hl7.org/fhir/R4/patient.html) - FHIR specification for Patient - [HL7 C-CDA Specification](https://www.hl7.org/implement/standards/product_brief.cfm?product_id=492) - Official C-CDA implementation guide --- ## ChargeItemDefinition $apply The `$apply` operation applies a `ChargeItemDefinition` resource to a specific `ChargeItem`, calculating the appropriate pricing based on the definition's property groups and applicability rules. ### Use Cases - **Automated Pricing Calculations**: Automatically calculate prices for lab tests, procedures, or services based on predefined rules - **Insurance-Specific Rates**: Apply different pricing based on payer contracts or insurance types - **Dynamic Discount Management**: Calculate surcharges or discounts based on patient eligibility or service context - **Revenue Cycle Automation**: Streamline billing workflows by programmatically determining charge amounts ### Invocation ``` POST [base]/ChargeItemDefinition/[id]/$apply ``` ### Input Parameters | Parameter | Cardinality | Type | Description | |-----------|-------------|------|-------------| | `chargeItem` | 1..1 | `Reference` | Reference to the ChargeItem to which pricing should be applied | ### Output The operation returns the updated `ChargeItem` resource with the `priceOverride` field set based on the `ChargeItemDefinition`'s pricing rules. ### Behavior The operation processes the `ChargeItemDefinition`'s `propertyGroup` elements to determine pricing: 1. **Base Price**: Finds the first applicable property group with a `priceComponent` of type `base` and uses its amount 2. **Applicability**: Evaluates FHIRPath expressions in `applicability` conditions against the `ChargeItem` to determine if a property group applies 3. **Price Modifiers**: Applies `surcharge` and `discount` price components: - **Surcharge**: Adds the amount or calculates `basePrice * factor` - **Discount**: Subtracts the amount or calculates `basePrice * factor` ### Example #### Request ```http POST /fhir/R4/ChargeItemDefinition/lab-panel-pricing/$apply Content-Type: application/fhir+json { "resourceType": "Parameters", "parameter": [ { "name": "chargeItem", "valueReference": { "reference": "ChargeItem/abc123" } } ] } ``` #### Response ```json { "resourceType": "ChargeItem", "id": "abc123", "status": "billable", "code": { "coding": [ { "system": "http://example.org/lab-codes", "code": "PANEL001" } ] }, "subject": { "reference": "Patient/patient123" }, "priceOverride": { "value": 150.00, "currency": "USD" } } ``` ### Error Responses | Status Code | Description | |-------------|-------------| | `400 Bad Request` | Missing required `chargeItem` parameter | | `404 Not Found` | ChargeItemDefinition or ChargeItem not found | | `403 Forbidden` | Insufficient permissions | ### Related Documentation - [FHIR ChargeItemDefinition $apply Operation](https://www.hl7.org/fhir/chargeitemdefinition-operation-apply.html) - [ChargeItemDefinition Resource](https://www.hl7.org/fhir/chargeitemdefinition.html) - [ChargeItem Resource](https://www.hl7.org/fhir/chargeitem.html) --- ## Claim $export The `$export` operation exports a `Claim` resource as a CMS-1500 PDF document. This is useful for generating standardized claim forms for healthcare billing. ### Use Cases - **Insurance Claim Submission**: Generate CMS-1500 forms for submission to insurance payers - **Medical Billing Documentation**: Create standardized billing documents for patient records - **Claims Reconciliation**: Generate PDF copies of claims for auditing and reconciliation - **Patient Billing Records**: Provide patients with copies of claims submitted on their behalf ### Invocation #### Instance Level (GET) ``` GET [base]/Claim/[id]/$export ``` #### Type Level (POST) ``` POST [base]/Claim/$export ``` ### Input Parameters #### GET Request No parameters required - the Claim is identified by the URL path. #### POST Request | Parameter | Cardinality | Type | Description | |-----------|-------------|------|-------------| | `resource` | 1..1 | `Claim` | The Claim resource to export as PDF | ### Output Returns a `Media` resource containing a reference to the generated PDF document stored as a `Binary` resource. | Field | Description | |-------|-------------| | `Media.status` | `completed` | | `Media.subject` | Reference to the patient from the Claim | | `Media.operator` | Reference to the provider from the Claim | | `Media.issued` | Timestamp of PDF generation | | `Media.content.contentType` | `application/pdf` | | `Media.content.url` | Reference to the Binary containing the PDF | | `Media.content.title` | `cms-1500.pdf` | ### Example #### GET Request ```http GET /fhir/R4/Claim/claim123/$export ``` #### POST Request ```http POST /fhir/R4/Claim/$export Content-Type: application/fhir+json { "resourceType": "Parameters", "parameter": [ { "name": "resource", "resource": { "resourceType": "Claim", "status": "active", "type": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/claim-type", "code": "professional" } ] }, "use": "claim", "patient": { "reference": "Patient/patient123" }, "provider": { "reference": "Organization/org456" }, "created": "2026-01-08", "priority": { "coding": [ { "code": "normal" } ] } } } ] } ``` #### Response ```json { "resourceType": "Media", "status": "completed", "subject": { "reference": "Patient/patient123" }, "operator": { "reference": "Organization/org456" }, "issued": "2026-01-08T15:30:00.000Z", "content": { "contentType": "application/pdf", "url": "Binary/binary789", "title": "cms-1500.pdf" } } ``` ### Downloading the PDF To download the actual PDF file, retrieve the Binary resource referenced in the Media: ```http GET /fhir/R4/Binary/binary789 Accept: application/pdf ``` ### Error Responses | Status Code | Description | |-------------|-------------| | `400 Bad Request` | Claim ID is required (GET) or resource parameter missing (POST) | | `404 Not Found` | Claim not found | | `403 Forbidden` | Insufficient permissions | ### Notes - This operation is marked as **experimental** - The generated PDF follows the CMS-1500 form format - The Binary resource containing the PDF is automatically created during the operation ### Related Documentation - [Claim Resource](https://www.hl7.org/fhir/claim.html) - [Media Resource](https://www.hl7.org/fhir/media.html) - [Binary Resource](https://www.hl7.org/fhir/binary.html) --- ## ClientApplication $smart-launch The `$smart-launch` operation initiates a SMART on FHIR app launch sequence for a `ClientApplication`. This creates a `SmartAppLaunch` context resource and redirects the user to the application's launch URI. ### Use Cases - **EHR-Embedded App Launching**: Launch SMART apps from within an EHR's clinical workflow context - **Single Sign-On Integration**: Provide seamless authentication when launching third-party clinical apps - **Context-Aware App Initialization**: Pass patient or encounter context to SMART apps automatically - **Patient Portal Integration**: Enable patients to launch apps from their portal with proper authorization ### Invocation ``` GET [base]/ClientApplication/[id]/$smart-launch POST [base]/ClientApplication/[id]/$smart-launch ``` ### Input Parameters | Parameter | Cardinality | Type | Description | | ----------- | ----------- | ------ | ------------------------------------------------- | | `patient` | 0..1 | `uuid` | The patient ID to include in the launch context | | `encounter` | 0..1 | `uuid` | The encounter ID to include in the launch context | **Note:** Only one of `patient` or `encounter` can be specified, not both. ### Output The operation returns an HTTP redirect (302) to the ClientApplication's `launchUri` with the following query parameters: | Parameter | Description | | --------- | ------------------------------------------------------------------- | | `iss` | The FHIR server base URL (e.g., `https://api.medplum.com/fhir/R4/`) | | `launch` | The ID of the created `SmartAppLaunch` resource | ### Prerequisites The `ClientApplication` must have a `launchUri` configured. This is the URL where the SMART app expects to receive the launch request. It must be an absolute `http://` or `https://` URL; other schemes are rejected. ### Example #### Request ```http GET /fhir/R4/ClientApplication/my-smart-app/$smart-launch?patient=patient123 ``` #### Response ```http HTTP/1.1 302 Found Location: https://my-smart-app.example.com/launch?iss=https://api.medplum.com/fhir/R4/&launch=launch456 ``` #### Created SmartAppLaunch Resource The operation creates a `SmartAppLaunch` resource in the background: ```json { "resourceType": "SmartAppLaunch", "id": "launch456", "patient": { "reference": "Patient/patient123" } } ``` ### SMART App Launch Flow 1. **Initiate Launch**: Call `$smart-launch` on the ClientApplication 2. **Redirect**: User is redirected to the app's `launchUri` with `iss` and `launch` parameters 3. **Authorization**: The app uses the `launch` parameter during OAuth authorization to retrieve the launch context 4. **Token Exchange**: After authorization, the app can exchange the code for tokens and retrieve the launch context ### Error Responses | Status Code | Description | | ----------------- | ----------------------------------------------------------------- | | `400 Bad Request` | ClientApplication ID not specified | | `400 Bad Request` | ClientApplication not configured for launch (missing `launchUri`) | | `400 Bad Request` | `launchUri` is not an absolute `http://` or `https://` URL | | `400 Bad Request` | Both `patient` and `encounter` specified (only one allowed) | | `404 Not Found` | ClientApplication not found | ### Configuration To enable SMART launch for a ClientApplication, set the `launchUri` field: ```json { "resourceType": "ClientApplication", "id": "my-smart-app", "name": "My SMART App", "launchUri": "https://my-smart-app.example.com/launch" } ``` ### Related Documentation - [SMART on FHIR Implementation Guide](http://hl7.org/fhir/smart-app-launch/) - [ClientApplication Resource](/docs/api/fhir/medplum/clientapplication) - [OAuth and Authentication](/docs/auth) --- ## CodeSystem $import import ExampleCode from '!!raw-loader!@site/../examples/src/api/fhir/operations/codesystem-import.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; Large standard terminologies like [**SNOMED CT**](https://www.snomed.org/what-is-snomed-ct) (350k+ codes) and [**LOINC**](https://loinc.org/about/) (100k+ codes) are too large to manage within a single FHIR resource. The `$import` operation bulk-loads codes, display names, and metadata into a CodeSystem, enabling you to work with these essential terminologies efficiently. ### Use Cases - **Loading standard terminologies**: Import SNOMED CT, LOINC, RxNorm, or ICD code systems into your Medplum instance - **Custom code system management**: Create and populate organization-specific code systems for internal coding standards - **Terminology updates**: Incrementally add new codes or update metadata when new terminology versions are released - **Migration and synchronization**: Import codes from existing terminology servers or legacy systems ``` [baseUrl]/CodeSystem/$import [baseUrl]/CodeSystem/[id]/$import ``` :::warning[Privileged Operation] This operation is only available to Project Admins and Super Admins. ::: ### Parameters | Name | Type | Description | Required | | ------------- | -------------------- | ------------------------------------------------------------------ | --------------- | | `system` | `uri` | The canonical URL of the code system to import into | No\* | | `concept` | `Coding` | A code, along with optional display text to add to the code system | No | | `property` | `Part[]` (see below) | Metadata about codes in the code system to be imported | No | | `designation` | `Part[]` (see below) | Additional synonyms or translations of codes' display text | No | :::note[] \* If `system` is not provided, the operation must be invoked on a specific `CodeSystem` instance by ID. ::: Each `property` parameter contains a `part` array with three nested parameters: | Name | Type | Description | Required | | ---------- | -------- | --------------------------------------------------- | -------- | | `code` | `code` | The code to which the property applies | Yes | | `property` | `code` | The property name (from `CodeSystem.property.code`) | Yes | | `value` | `string` | The value of the property | Yes | Any `designation` parameters have three nested `part` parameters: | Name | Type | Description | Required | | ---------- | -------- | ---------------------------------------------------- | -------- | | `code` | `code` | The code to which the synonym or translation applies | Yes | | `language` | `code` | The [language code][bcp47] for translations | No | | `value` | `string` | Display string | Yes | :::info[Batch size] Each invocation of the `$import` operation can send a maximum of 1,000 of each parameter at a time, e.g. importing 1,000 concepts with one property and one designation each per request. ::: [bcp47]: https://developer.mozilla.org/en-US/docs/Glossary/BCP_47_language_tag ### Output The operation returns the `CodeSystem` resource representing the code system into which the given concepts and properties were imported. ### Examples **Request**: {ExampleCode} ### Related - [CodeSystem $lookup](/docs/api/fhir/operations/codesystem-lookup) - Look up code details after importing - [CodeSystem $validate-code](/docs/api/fhir/operations/codesystem-validate-code) - Validate codes within imported code systems - [ValueSet $expand](/docs/api/fhir/operations/valueset-expand) - Expand value sets that reference imported code systems - [Medplum Terminology Guide](/docs/terminology) - Overview of terminology services in Medplum - [FHIR CodeSystem Resource](https://hl7.org/fhir/R4/codesystem.html) - FHIR specification for CodeSystem --- ## CodeSystem $lookup The `$lookup` operation retrieves detailed information about a specific code from a code system. This is essential for applications that need to display human-readable descriptions, validate code properties, or understand the relationships and metadata associated with clinical terminology codes. When working with healthcare standards like LOINC, SNOMED CT, or ICD-10, `$lookup` helps you access display names, synonyms, parent/child relationships, and other properties that make coded data meaningful to clinicians and patients. ### Use Cases - **Display Code Descriptions**: Show human-readable names for lab tests, diagnoses, or procedures in your UI, including alternative names or consumer-friendly descriptions for medical concepts - **Code Property Retrieval**: Access metadata like LOINC component, property, timing, and scale information - **Hierarchy Navigation**: Discover parent codes to understand where a concept fits in the terminology hierarchy ### Invoke the `$lookup` operation ``` [baseUrl]/CodeSystem/$lookup [baseUrl]/CodeSystem/[id]/$lookup ``` ### Parameters | Name | Type | Description | Required | | ---------- | -------- | --------------------------------------------------------- | -------- | | `code` | `string` | The code to look up. | No | | `system` | `string` | The canonical URL of the code system the code belongs to. | No | | `version` | `string` | The version of the code system to search. | No | | `coding` | `Coding` | Look up via full Coding. | No | | `property` | `string` | Properties of the coding to return in the response. | No | :::note[Required Parameters] Although no single parameter is required by the operation, at least one of the following must be provided: - Both `code` and `system` parameters (and optionally `version` as well) - The `coding` parameter If the operation is invoked on a specific `CodeSystem` instance (i.e. calling `/CodeSystem/[id]/$lookup`), only `code` or `coding.code` need to be provided. ::: ### Output The operation returns a `Parameters` resource containing the resolved information for the code. | Parameter Name | Type | Description | Required | | -------------- | -------------------- | ------------------------------------------------------ | -------- | | `name` | `string` | The full name of the code system | Yes | | `display` | `string` | The display string associated with the code | Yes | | `property` | `Part[]` (see below) | A property associated with the code by the code system | No | Any `property` parameters contain 2-3 nested `part` parameters: | Parameter Name | Type | Description | Required | | -------------- | -------- | ----------------------------------------------- | -------- | | `code` | `code` | The name of the parameter | Yes | | `value` | \* | The value of the property | Yes | | `description` | `string` | What the property means, or how to interpret it | No | **Request**: ```http GET /CodeSystem/$lookup?system=http://loinc.org&code=8867-4 ``` **Response** (200 OK):
Response JSON ```js { "resourceType": "Parameters", "parameter": [ { "name": "name", "valueString": "LOINC Code System" }, { "name": "display", "valueString": "Heart rate" }, { "name": "property", "part": [ { "name": "code", "valueCode": "LN" }, { "name": "value", "valueString": "Heart rate:NRat:Pt:XXX:Qn" }, { "name": "description", "valueString": "LOINC official fully specified name" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "parent" }, { "name": "value", "valueCode": "MTHU000084" }, { "name": "description", "valueString": "A parent code in the Component Hierarchy by System" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "parent" }, { "name": "value", "valueCode": "LP415756-8" }, { "name": "description", "valueString": "A parent code in the Component Hierarchy by System" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "STATUS" }, { "name": "value", "valueString": "ACTIVE" }, { "name": "description", "valueString": "Status of the term. Within LOINC, codes with STATUS=DEPRECATED are considered inactive. Current values: ACTIVE, TRIAL, DISCOURAGED, and DEPRECATED" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "CLASS" }, { "name": "value", "valueString": "HRTRATE.ATOM" }, { "name": "description", "valueString": "An arbitrary classification of terms for grouping related observations together" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "COMPONENT" }, { "name": "value", "valueString": "Heart rate" }, { "name": "description", "valueString": "First major axis-component or analyte: Analyte Name, Analyte sub-class, Challenge" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "CLASSTYPE" }, { "name": "value", "valueString": "2" }, { "name": "description", "valueString": "1=Laboratory class; 2=Clinical class; 3=Claims attachments; 4=Surveys" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "COMMON_TEST_RANK" }, { "name": "value", "valueString": "18" }, { "name": "description", "valueString": "Ranking of approximately 2000 common tests performed by laboratories in USA." } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "SYSTEM" }, { "name": "value", "valueString": "XXX" }, { "name": "description", "valueString": "Fourth major axis-type of specimen or system: System (Sample) Type" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "PROPERTY" }, { "name": "value", "valueString": "NRat" }, { "name": "description", "valueString": "Second major axis-property observed: Kind of Property (also called kind of quantity)" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "TIME_ASPCT" }, { "name": "value", "valueString": "Pt" }, { "name": "description", "valueString": "Third major axis-timing of the measurement: Time Aspect (Point or moment in time vs. time interval)" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "SCALE_TYP" }, { "name": "value", "valueString": "Qn" }, { "name": "description", "valueString": "Fifth major axis-scale of measurement: Type of Scale" } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "CONSUMER_NAME" }, { "name": "value", "valueString": "Heart rate" }, { "name": "description", "valueString": "An experimental (beta) consumer friendly name for this item. The intent is to provide a test name that health care consumers will recognize." } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "RELATEDNAMES2" }, { "name": "value", "valueString": "Count/time; Heart beat; HEART RATE.ATOM; Misc; Miscellaneous; nRate; Number rate; Number Rate = Count/Time; Other; Point in time; Pulse; QNT; Quan; Quant; Quantitative; Random; Spec; To be specified in another part of the message; Unspecified" }, { "name": "description", "valueString": "This field was introduced in version 2.05. It contains synonyms for each of the parts of the fully specified LOINC name (component, property, time, system, scale, method)." } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "EXAMPLE_UNITS" }, { "name": "value", "valueString": "beats/min" }, { "name": "description", "valueString": "This field is populated with a combination of submitters units and units that people have sent us. Its purpose is to show users representative, but not necessarily recommended, units in which data could be sent for this term." } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "EXAMPLE_UCUM_UNITS" }, { "name": "value", "valueString": "{beats}/min;{counts/min}" }, { "name": "description", "valueString": "The Unified Code for Units of Measure (UCUM) is a code system intended to include all units of measures being contemporarily used in international science, engineering, and business. (www.unitsofmeasure.org) This field contains example units of measures for this term expressed as UCUM units." } ] }, { "name": "property", "part": [ { "name": "code", "valueCode": "AssociatedObservations" }, { "name": "value", "valueString": "89263-8" }, { "name": "description", "valueString": "A multi-valued, semicolon delimited list of LOINC codes that represent optional associated observation(s) for a clinical observation or laboratory test. A LOINC term in this field may represent a single associated observation or panel containing several associated observations." } ] } ] } ```
#### Error Response If the given code could not be found within the target code system, or any other error occurs, the server will return an `OperationOutcome` resource containing details about the error. **Request**: ```http GET /CodeSystem/$lookup?system=http://loinc.org&code=invalid ``` **Response** (404 Not Found): ```js { "resourceType": "OperationOutcome", "id": "not-found", "issue": [{ "severity": "error", "code": "not-found", "details": { "text": "Not found" } }], } ``` ### Related - [CodeSystem $validate-code](/docs/api/fhir/operations/codesystem-validate-code) - Validate a code exists in a code system - [CodeSystem $subsumes](/docs/api/fhir/operations/codesystem-subsumes) - Check hierarchical relationships between codes - [CodeSystem $import](/docs/api/fhir/operations/codesystem-import) - Import codes into a code system - [Medplum Terminology Guide](/docs/terminology) - Overview of terminology services - [FHIR CodeSystem $lookup](https://hl7.org/fhir/R4/codesystem-operation-lookup.html) - FHIR specification for $lookup --- ## CodeSystem $subsumes The `$subsumes` operation tests whether one code is a parent (more general) or child (more specific) of another code within a hierarchical code system. This is crucial for clinical decision support, where you need to determine if a specific diagnosis or procedure falls under a broader category. For example, you can use `$subsumes` to check if a patient's specific heart condition code is a subtype of "cardiac disease," enabling rule-based alerts and automated clinical workflows that respond to categories of conditions, rather than exhaustive lists of individual codes. ### Use Cases - **Clinical Decision Support**: Trigger alerts when any code within a disease category is recorded (e.g., any cardiovascular condition) - **Quality Measure Calculation**: Determine if a patient's diagnoses qualify for specific quality measures by checking category membership - **Access Control**: Grant access to records based on condition categories rather than specific codes - **Report Aggregation**: Group detailed codes into broader categories for population health reporting - **Formulary Management**: Check if a prescribed medication falls within a therapeutic class ### Invoke the `$subsumes` operation ``` [baseUrl]/CodeSystem/$subsumes [baseUrl]/CodeSystem/[id]/$subsumes ``` ### Parameters | Name | Type | Description | Required | | --------- | -------- | --------------------------------------------------------- | --------------- | | `system` | `string` | The canonical URL of the code system the codes belong to. | No\* | | `version` | `string` | The version of the code system to search. | No | | `codeA` | `code` | One of the codes to test. | Yes | | `codeB` | `code` | The other code to test. | Yes | \* If no `system` is specified, the operation must be invoked on a specific `CodeSystem` instance by ID. ### Output The operation returns a `Parameters` resource containing the resolved information for the code. | Parameter Name | Type | Description | Required | | -------------- | ------ | -------------------------------------------------------------------------- | -------- | | `outcome` | `code` | A code describing the relationship between `codeA` and `codeB` (see below) | Yes | The possible `outcome` values are: | Outcome | Description | | -------------- | ------------------------------------------------------------- | | `equivalent` | The two codes are the same, or mean the same thing | | `subsumes` | Code `A` is the more general parent of more specific code `B` | | `subsumed-by` | Code `A` is the more specific child of more general code `B` | | `not-subsumed` | Codes `A` and `B` are not directly related | **Request**: To check whether SNOMED code `364075005` (Heart rate) is a descendant code of `363787002` (Observable entity): ```http GET https://api.medplum.com/fhir/R4/CodeSystem/$subsumes?system=http://snomed.info/sct&codeA=364075005&codeB=363787002 ``` ```bash curl 'https://api.medplum.com/fhir/R4/CodeSystem/$subsumes' \ --get \ -H "Authorization: Bearer $MY_ACCESS_TOKEN" \ -d 'system=http://snomed.info/sct' \ -d 'codeA=364075005' \ -d 'codeB=363787002' ``` **Response** (200 OK): ```js { "resourceType": "Parameters", "parameter": [ { "name": "outcome", "valueCode": "subsumed-by" } ] } ``` ### Related - [CodeSystem $lookup](/docs/api/fhir/operations/codesystem-lookup) - Look up detailed information about codes - [CodeSystem $validate-code](/docs/api/fhir/operations/codesystem-validate-code) - Validate a code exists in a code system - [CodeSystem $import](/docs/api/fhir/operations/codesystem-import) - Import hierarchical code systems - [Medplum Terminology Guide](/docs/terminology) - Overview of terminology services - [FHIR CodeSystem $subsumes](https://hl7.org/fhir/R4/codesystem-operation-subsumes.html) - FHIR specification for $subsumes --- ## CodeSystem $validate-code Validating codes at the point of entry is necessary to prevent data quality issues that can cascade through analytics, clinical decision support, and interoperability workflows. Invalid codes can cause workflow failures, incorrect reports, and missed clinical alerts - all of which degrade clinical experiences. The `$validate-code` operation checks whether a code exists within a specific code system and returns its display text if found. This is fundamental for data quality-ensuring that clinical data uses valid, recognized codes before storing or processing it. ### Use Cases - **Form and Input Validation**: Verify user-entered codes or confirm selected codes from autocomplete/typeahead are valid before saving clinical data - **Interface Validation**: Check incoming codes from external systems (labs, pharmacies, EHRs) before processing - **Data Quality Checks**: Audit existing data to identify invalid or deprecated codes - **Migration Validation**: Ensure codes are valid when importing data from legacy systems ### Invoke the `$validate-code` operation ``` [baseUrl]/CodeSystem/$validate-code [baseUrl]/CodeSystem/[id]/$validate-code ``` ### Parameters | Name | Type | Description | Required | | --------- | -------- | --------------------------------------------------------- | --------------------- | | `url` | `uri` | The canonical URL of the `CodeSystem` to validate against | No\* | | `version` | `string` | The version of the code system to search. | No | | `code` | `string` | The code to look up. | No† | | `coding` | `Coding` | Look up via full Coding. | No† | \* If no `url` is provided, the operation must be invoked on a specific `ValueSet` instance. † One of `code` or `coding` must be provided. ### Output The operation returns a `Parameters` resource containing the validation result. | Name | Type | Description | Required | | --------- | --------- | -------------------------------------------------------- | -------- | | `result` | `boolean` | Whether or not the coding is from the given `CodeSystem` | Yes | | `display` | `string` | The display text of the included code | No | ### Examples **Request**: ```bash curl 'https://api.medplum.com/fhir/R4/CodeSystem/$validate-code' \ --get \ -H "Authorization: Bearer $MY_ACCESS_TOKEN" \ -d 'url=http://snomed.info/sct' \ -d 'code=255604002' ``` **Response**: (200 OK) ```js { "resourceType": "Parameters", "parameter": [ { "name": "result", "valueBoolean": true }, { "name": "display", "valueString": "Mild (qualifier value)" } ] } ``` ### Related - [ValueSet $validate-code](/docs/api/fhir/operations/valueset-validate-code) - Validate codes against value sets - [CodeSystem $lookup](/docs/api/fhir/operations/codesystem-lookup) - Get detailed information about a code - [CodeSystem $subsumes](/docs/api/fhir/operations/codesystem-subsumes) - Check hierarchical relationships - [Medplum Terminology Guide](/docs/terminology) - Overview of terminology services - [FHIR CodeSystem $validate-code](https://hl7.org/fhir/R4/codesystem-operation-validate-code.html) - FHIR specification --- ## ConceptMap $import The `$import` operation allows you to import code mappings into a ConceptMap. This is useful for bulk loading terminology mappings from external sources or programmatically adding mappings to an existing ConceptMap. ### Use Cases - **Bulk Import from External Sources**: Import mappings from other terminology systems or external standards organizations, including programmatically creating mappings from external mapping files - **Mapping Updates**: Update existing mappings with new versions ### Authorization This operation requires **admin privileges**. You must be a project admin (`ProjectMembership.admin = true`). ### Invoke the `$import` operation #### By Instance ID ``` [base]/ConceptMap/[id]/$import ``` #### By URL (Type Level) ``` [base]/ConceptMap/$import ``` ### Parameters | Name | Type | Description | Required | | --------- | --------- | --------------------------------------------------------- | -------- | | `url` | `uri` | The canonical URL of the ConceptMap to import into | No* | | `mapping` | `complex` | One or more mappings to import (see structure below) | Yes | :::note * Either the instance ID in the URL or the `url` parameter must be provided. ::: #### Mapping Structure Each mapping has the following parts: | Part | Type | Description | Required | | -------------- | -------- | ----------------------------------------------------------- | -------- | | `source` | `Coding` | The source code (system, code, display) | Yes | | `target` | `Coding` | The target code (system required; code optional for null map) | Yes | | `relationship` | `code` | Equivalence type (default: `equivalent`) | No | | `comment` | `string` | Comment about the mapping | No | | `property` | `array` | Additional properties on the mapping | No | | `dependsOn` | `array` | Dependencies for the mapping | No | | `product` | `array` | Products of the mapping | No | #### Relationship Values Valid values from the [concept-map-equivalence](http://hl7.org/fhir/R4/valueset-concept-map-equivalence.html) ValueSet: - `equivalent` (default) - `equal` - `wider` - `narrower` - `inexact` - `unmatched` - `disjoint` ### Example Request ```bash curl -X POST 'https://api.medplum.com/fhir/R4/ConceptMap/my-concept-map/$import' \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "mapping", "part": [ { "name": "source", "valueCoding": { "system": "http://example.org/source-system", "code": "ABC", "display": "Source Code ABC" } }, { "name": "target", "valueCoding": { "system": "http://example.org/target-system", "code": "XYZ", "display": "Target Code XYZ" } }, { "name": "relationship", "valueCode": "equivalent" } ] }, { "name": "mapping", "part": [ { "name": "source", "valueCoding": { "system": "http://example.org/source-system", "code": "DEF", "display": "Source Code DEF" } }, { "name": "target", "valueCoding": { "system": "http://example.org/target-system", "code": "UVW", "display": "Target Code UVW" } } ] } ] }' ``` ### Output The operation returns the ConceptMap resource that was imported into. #### Example Response ```json { "resourceType": "ConceptMap", "id": "my-concept-map", "url": "http://example.org/ConceptMap/my-mappings", "status": "active", } ``` ### Behavior - **Upsert Logic**: Mappings are inserted or updated based on the unique combination of (conceptMap, sourceSystem, sourceCode, targetSystem, targetCode) - **Transaction**: The entire import runs within a database transaction - **Existing Mappings**: Mappings already defined in the ConceptMap resource are also imported into the database ### Error Responses #### URL Not Permitted for Instance Operation Occurs when calling the instance-level operation (`/ConceptMap/[id]/$import`) while also providing the `url` parameter. The target ConceptMap is already identified by the instance ID in the path, so `url` is redundant and not allowed. Omit the `url` parameter when using the instance URL, or call the type-level operation (`/ConceptMap/$import`) and provide `url` in the request body instead. ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "Parameter `url` not permitted for instance operation" } } ] } ``` ### Related Documentation - [ConceptMap $translate](/docs/api/fhir/operations/conceptmap-translate) - Translate codes using ConceptMaps - [Terminology](/docs/terminology) - Overview of terminology support in Medplum - [FHIR ConceptMap](http://hl7.org/fhir/R4/conceptmap.html) - FHIR specification --- ## ConceptMap $translate Code translation enables seamless data exchange between systems that speak different "clinical languages," ensuring that a diagnosis recorded in one system can be understood and processed correctly by another. The `$translate` operation converts codes from one code system to another using predefined mappings. This is essential for healthcare interoperability, where different systems use different terminologies-for example, translating SNOMED CT diagnoses to ICD-10 for billing, or mapping internal lab codes to LOINC for reporting. ### Use Cases - **Billing Code Translation**: Convert clinical SNOMED CT codes to ICD-10 codes for claims submission - **Lab Interface Mapping**: Translate internal lab system codes to standard LOINC codes for external reporting - **EHR Integration**: Map codes from one EHR's proprietary system to another during data migration or exchange - **Quality Reporting**: Convert clinical codes to the specific code systems required by quality measure programs - **Cross-System Analytics**: Normalize codes from multiple source systems to a common terminology for population health analysis [translate-operation]: http://hl7.org/fhir/R4/conceptmap-operation-translate.html ### Invoke the `$translate` operation ``` [base]/ConceptMap/$translate [base]/ConceptMap/[id]/$translate ``` #### Parameters | Name | Type | Description | Required | | ----------------- | ----------------- | ---------------------------------------------------------------- | -------- | | `url` | `uri` | Canonical URL of the ConceptMap, if not specified by ID | No | | `source` | `uri` | Canonical URL of source ValueSet, used to look up ConceptMap | No | | `code` | `code` | Code to translate using the ConceptMap | No | | `system` | `uri` | System the code (above) is drawn from | No | | `coding` | `Coding` | Full coding to translate using the ConceptMap | No | | `codeableConcept` | `CodeableConcept` | Concept with one or more codes to translate using the ConceptMap | No | | `targetsystem` | `uri` | Target code system, used to filter results | No | :::note[Required Parameters] Although no individual parameter for the operation is required, both of the following must be satisfied for the operation to proceed: - Either the ConceptMap ID must be present in the request path (e.g. by calling `ConceptMap/[id]/$translate`), or one of `url` or `source` must be populated to look up the ConceptMap - Exactly one of the following must be set to specify the source code(s) to translate: - Both `code` and `system` - `coding` - `codeableConcept` ::: #### Example As an example, assume the following `ConceptMap` resource exists in the system: ```json { "resourceType": "ConceptMap", "url": "http://example.com/concept-map", "status": "active", "sourceCanonical": "http://example.com/labs", "group": [ { "source": "http://snomed.info/sct", "target": "http://loinc.org", "element": [ { "code": "313444004", "target": [ { "code": "15067-2", "display": "Follitropin Qn", "equivalence": "equivalent" } ] } ] }, { "source": "http://snomed.info/sct", "target": "http://www.ama-assn.org/go/cpt", "element": [ { "code": "313444004", "target": [{ "code": "83001", "equivalence": "equivalent" }] } ] } ] } ``` To translate a code using this ConceptMap, one would make an API request like the following: ```bash curl 'https://api.medplum.com/fhir/R4/ConceptMap/[id]/$translate' \ -X POST \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -d '{"resourceType":"Parameters","parameter":[ {"name":"code","valueCode":"313444004"}, {"name":"system","valueUri":"http://snomed.info/sct"} ]}' ``` #### Success Response Example outcome: ```json { "resourceType": "Parameters", "parameter": [ { "name": "result", "valueBoolean": true }, { "name": "match", "part": [ { "name": "equivalence", "valueCode": "equivalent" }, { "name": "concept", "valueCoding": { "system": "http://loinc.org", "code": "15067-2", "display": "Follitropin Qn" }} ] } { "name": "match", "part": [ { "name": "equivalence", "valueCode": "equivalent" }, { "name": "concept", "valueCoding": { "system": "http://www.ama-assn.org/go/cpt", "code": "83001" }} ] } ] } ``` #### Error Response Example outcome when ambiguous input parameters are provided, e.g.: **Request**: ```http POST /fhir/R4/ConceptMap/$translate ``` ```json { "resourceType": "Parameters", "parameter": [ { "name": "url", "valueCode": "http://example.com/concept-map" }, { "name": "code", "valueCode": "foo" }, { "name": "system", "valueUri": "http://example.com/code-system" }, { "name": "coding", "valueCoding": { "code": "bar", "system": "http://example.com/code-system" } } ] } ``` **Response**: ```http 400 Bad Request ``` ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "Ambiguous input: multiple source codings provided" } } ] } ``` ### Related - [CodeSystem $lookup](/docs/api/fhir/operations/codesystem-lookup) - Look up code details in source or target systems - [CodeSystem $validate-code](/docs/api/fhir/operations/codesystem-validate-code) - Validate codes before translation - [Medplum Terminology Guide](/docs/terminology) - Overview of terminology services - [FHIR ConceptMap Resource](https://hl7.org/fhir/R4/conceptmap.html) - FHIR specification for ConceptMap - [FHIR $translate Operation](https://hl7.org/fhir/R4/conceptmap-operation-translate.html) - FHIR specification for $translate --- ## Resource $csv The `$csv` operation exports FHIR resources as a CSV (Comma-Separated Values) file, which is useful for exporting data for analysis in spreadsheet applications, reporting tools, or data pipelines. ### Use Cases - **Reporting**: Generate reports for administrative or clinical purposes - **Data Analysis**: Export data for analysis in Excel, Google Sheets, or BI tools - **Data Migration**: Export data for import into other systems - **Auditing**: Create snapshots of data for audit purposes ### Invoke the `$csv` operation ``` [base]/[resourceType]/$csv?_fields=[field1,field2,...]&[search parameters] ``` For example, to export Patient data: ```bash curl 'https://api.medplum.com/fhir/R4/Patient/$csv?_fields=name,birthDate,gender,address' \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -o patients.csv ``` ### Parameters | Name | Type | Description | Required | | ---------- | -------- | ---------------------------------------------------------- | -------- | | `_fields` | `string` | Comma-separated list of fields/search parameters to export | Yes | | *any* | `string` | Any valid search parameters for the resource type | No | #### Fields Parameter The `_fields` parameter accepts: - **Search parameter names** - These are resolved to their FHIRPath expressions - **FHIRPath expressions** - Direct FHIRPath expressions for custom field extraction #### Example Field Configurations ``` _fields=name,birthDate,gender _fields=identifier,name,telecom,address _fields=Patient.name.family,Patient.name.given ``` ### Output The operation returns a CSV file with: - UTF-8 encoding with BOM (Byte Order Marker) for Excel compatibility - Header row with column names - One row per resource - Automatic formatting for complex FHIR types #### Supported Data Type Formatting | FHIR Type | CSV Output | | ----------------- | ----------------------------------------- | | `HumanName` | Formatted full name | | `Address` | Formatted address string | | `ContactPoint` | The `value` field (phone, email, etc.) | | `Reference` | The `display` field | | `CodeableConcept` | Display text or first coding display/code | | `string` | Direct value | | `number` | Direct value | | `boolean` | "true" or "false" | ### Examples #### Export All Patients ```bash curl 'https://api.medplum.com/fhir/R4/Patient/$csv?_fields=name,birthDate,gender,address' \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -o all-patients.csv ``` #### Export Filtered Results Export only active patients: ```bash curl 'https://api.medplum.com/fhir/R4/Patient/$csv?_fields=name,birthDate,gender&active=true' \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -o active-patients.csv ``` #### Export Observations Export vital signs observations: ```bash curl 'https://api.medplum.com/fhir/R4/Observation/$csv?_fields=code,value-quantity,effective-date-time,subject&category=vital-signs' \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -o vitals.csv ``` #### Export with Custom FHIRPath ```bash curl 'https://api.medplum.com/fhir/R4/Patient/$csv?_fields=name.family,name.given,telecom.where(system=%27phone%27).value' \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -o patients-custom.csv ``` ### Response Headers ``` Content-Type: text/csv Content-Disposition: attachment; filename=export.csv ``` ### Limitations - Maximum 1000 resources per export - For larger exports, use the [Bulk FHIR](/docs/api/fhir/operations/bulk-fhir) operations - Only the first value is exported for multi-valued fields ### Error Responses #### Missing Fields Parameter ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "Missing _fields parameter" } } ] } ``` #### Invalid Resource Type ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "Unsupported resource type" } } ] } ``` ### Security The operation includes CSV injection protection by escaping potentially dangerous characters (`=`, `+`, `-`, `@`) at the start of cell values. ### Related Documentation - [Bulk FHIR Export](/docs/api/fhir/operations/bulk-fhir) - For large-scale data exports - [Search](/docs/search) - FHIR search parameters --- ## Custom Operations Medplum supports custom FHIR operations implemented via Bots. This allows you to define your own operations with custom business logic while following FHIR operation semantics. ### Overview Custom operations enable you to: - **Build domain-specific APIs**: Create custom endpoints tailored to your organization's specific healthcare workflows - **Implement business logic**: Add complex validation, calculation, or transformation logic as reusable operations - **Extend FHIR capabilities**: Add functionality beyond standard FHIR operations while maintaining API consistency - **Create integration endpoints**: Receive data from external systems in specific formats ### Quick Example Custom operations are created by: 1. Creating a `Bot` resource with your JavaScript implementation 2. Creating an `OperationDefinition` resource that defines the operation's interface 3. Linking them using a Medplum-specific extension Once configured, operations can be invoked like any standard FHIR operation: ```http POST [base]/[ResourceType]/$my-operation GET [base]/[ResourceType]/[id]/$my-operation ``` ### Full Documentation For complete documentation on creating and using custom FHIR operations, see: **[Custom FHIR Operations Guide](/docs/bots/custom-fhir-operations)** The full guide covers: - Creating and deploying Bots for custom operations - Defining OperationDefinition resources with proper parameters - Configuring system, type, and instance-level operations - Input and output handling for POST and GET requests - Security and permissions model - Error handling patterns - Complete working examples ### Related Documentation - [Bot Basics](/docs/bots/bot-basics) - [OperationDefinition Resource](https://www.hl7.org/fhir/operationdefinition.html) - [FHIR Operations](https://www.hl7.org/fhir/operations.html) --- ## Measure $evaluate-measure The `$evaluate-measure` operation executes a defined clinical quality measure against your patient population and returns aggregated results. This enables healthcare organizations to programmatically calculate quality metrics, track clinical outcomes, and generate reports for regulatory compliance without manual data extraction. **Use Cases:** - **Quality reporting**: Calculate eCQM (electronic Clinical Quality Measures) for CMS reporting, MIPS, or other regulatory requirements - **Population health analytics**: Track clinical outcomes across patient populations to identify care gaps and improvement opportunities - **Performance dashboards**: Generate real-time metrics for provider performance tracking and quality improvement initiatives - **Research cohort analysis**: Count and analyze patient populations meeting specific clinical criteria for research studies Medplum supports the Measure [$evaluate-measure](https://hl7.org/fhir/R4/measure-operation-evaluate-measure.html) operation. :::caution[] `$evaluate-measure` is early and limited. At present, it only supports calculating counts using "application/x-fhir-query". Using FHIR `Library` is currently unsupported. ::: ### Usage First, create a FHIR `Measure`: ```json { "resourceType": "Measure", "status": "active", "url": "https://example.com/test-measure", "group": [ { "population": [ { "code": { "coding": [ { "code": "measure-population" } ] }, "criteria": { "language": "application/x-fhir-query", "expression": "Patient" } } ] } ] } ``` Next, make an HTTP POST request to the `$evaluate-measure` endpoint: ```bash curl 'https://api.medplum.com/fhir/R4/Measure/MY_MEASURE_ID/$evaluate-measure' \ -X POST \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -d '{"resourceType":"Parameters","parameter":[{"name":"periodStart","valueDate":"2020-01-01"},{"name":"periodEnd","valueDate":"2030-01-01"}]}' ``` The result will be a FHIR `MeasureReport` resource: ```json { "resourceType": "MeasureReport", "id": "b40f97d9-385f-4336-8c6c-8892160a7fa1", "meta": { "versionId": "182948fb-e287-4f05-a313-1d6df883ee0f", "lastUpdated": "2023-05-15T02:23:22.301Z" }, "status": "complete", "type": "summary", "measure": "https://example.com/test-measure", "date": "2023-05-15T02:23:22.298Z", "period": { "start": "2020-01-01", "end": "2030-01-01" }, "group": [ { "population": [ { "code": { "coding": [ { "code": "measure-population" } ] }, "count": 1000 } ] } ] } ``` ### Related - [FHIR Measure Resource](https://hl7.org/fhir/R4/measure.html) - FHIR specification for Measure - [FHIR MeasureReport Resource](https://hl7.org/fhir/R4/measurereport.html) - FHIR specification for MeasureReport - [FHIR $evaluate-measure](https://hl7.org/fhir/R4/measure-operation-evaluate-measure.html) - FHIR specification for the operation - [CMS eCQM Information](https://ecqi.healthit.gov/ecqms) - Electronic clinical quality measures resources - [Medplum Analytics Guide](/docs/analytics) - Building analytics and dashboards with Medplum --- ## Resource $expunge The `$expunge` operation permanently deletes a resource and its history from the database. A minimal tombstone remains in the resource's history table. Its `meta.tag` uses the FHIR audit lifecycle code `destroy`, and it contains no resource data. :::warning[] This operation is **irreversible**. Once a resource is expunged, it cannot be recovered. Use with caution. ::: ### Use Cases - **Data Cleanup**: Remove test data from development environments - **Compliance**: Fulfill data deletion requests under regulations like GDPR - **Tenant Offboarding**: Clean up all data when a tenant leaves the platform - **Error Correction**: Remove erroneously created resources that shouldn't exist ### Authorization This operation requires **admin privileges**. You must be a project admin (`Membership.admin = true`). ### Invoke the `$expunge` operation #### Single Resource ``` [base]/[resourceType]/[id]/$expunge ``` For example, to expunge a single Patient: ```bash curl -X POST 'https://api.medplum.com/fhir/R4/Patient/example-id/$expunge' \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer MY_ACCESS_TOKEN" ``` #### Parameters | Name | Type | Description | Required | | ------------ | --------- | ------------------------------------------------------------ | -------- | | `everything` | `boolean` | Expunge all resources in the compartment (for Projects only) | No | #### Expunge Everything (Project Compartment) When expunging a Project, or when `everything=true`, Medplum starts an async job that walks **every FHIR resource type** in the target compartment (currently Patient and Project compartments). For each type it searches `_compartment`, then hard-deletes matching rows in batches of 10,000, including associated Binary storage. The HTTP request returns `202 Accepted` immediately; poll the `AsyncJob` for completion. Each deleted id still gets a history tombstone, including `AuditEvent` rows in the compartment. ```bash curl -X POST 'https://api.medplum.com/fhir/R4/Project/project-id/$expunge?everything=true' \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer MY_ACCESS_TOKEN" ``` ### Response #### Successful Single Resource Expunge ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "information", "code": "informational", "details": { "text": "All OK" } } ] } ``` #### Async Job Started (Everything Mode) When expunging a project or using `everything=true`, the operation returns immediately with a 202 Accepted status and a location header pointing to the async job: ``` HTTP/1.1 202 Accepted Content-Location: https://api.medplum.com/fhir/R4/AsyncJob/job-id ``` You can poll the AsyncJob to check the status: ```bash curl 'https://api.medplum.com/fhir/R4/AsyncJob/job-id' \ -H "Authorization: Bearer MY_ACCESS_TOKEN" ``` #### Access Denied If you don't have admin privileges: ```json { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "forbidden", "details": { "text": "Forbidden" } } ] } ``` ### Behavior #### Single Resource Expunge - Deletes the resource and all prior history versions - Writes a minimal history tombstone tagged with the ISO 21089 `destroy` lifecycle code - Removes associated Binary resources referenced by the resource - The operation is synchronous and returns immediately #### Everything Mode (Project Expunge) - Iterates through all resource types in the compartment - Deletes resources in batches of 10,000 - Also deletes associated Binary resources - Runs as an async job to handle large datasets - Leaves a history tombstone for each deleted id ### Related Documentation - [AsyncJob $cancel](/docs/api/fhir/operations/asyncjob-cancel) - Cancel a running expunge job - [FHIR Delete](/docs/api/fhir/operations) - Standard FHIR delete (creates tombstone) --- ## QuestionnaireResponse $extract The `$extract` operation transforms completed questionnaire responses into structured FHIR resources. This bridges the gap between form-based data collection and your clinical data model-automatically converting patient intake forms, assessments, or surveys into Observations, Conditions, Procedures, and other FHIR resources. This is a key component of the [Structured Data Capture][data-capture] workflow, allowing you to design forms that both collect information from users and populate your FHIR database without custom mapping code. ### Use Cases - **Patient Intake**: Convert registration forms into Patient, Coverage, and RelatedPerson resources - **Clinical Assessments**: Transform PHQ-9, GAD-7, or other assessment questionnaires into Observation resources with proper coding - **History Collection**: Extract past medical history responses into Condition resources - **Prior Authorization**: Generate CoverageEligbilityRequest and supporting documentation from PA request forms - **Research Data Collection**: Convert study questionnaires into Observation resources for analysis [template-extract]: https://build.fhir.org/ig/HL7/sdc/extraction.html#template-extract [sdc-ig]: https://build.fhir.org/ig/HL7/sdc/ [data-capture]: /docs/questionnaires/parsing-questionnaire-responses ### Invoke the `$extract` operation ``` [baseUrl]/QuestionnaireResponse/$extract [baseUrl]/QuestionnaireResponse/[id]/$extract ``` ### Parameters | Name | Type | Description | Required | | ------------------------ | ----------------------- | ------------------------------------------------- | -------------- | | `questionnaire-response` | `QuestionnaireResponse` | The response to parse | No1 | | `questionnaire` | `Questionnaire` | The Questionnaire to use for parsing the response | No2 | 1 If not called on a specific `QuestionnaireResponse` by ID, the response must be specified via the `questionnaire-response` parameter. 2 If `QuestionnaireResponse.questionnaire` is not specified in the resource, it must be passed in via the `questionnaire` parameter; otherwise it can be used to override the Questionnaire used for parsing. ### Output The operation returns a `Bundle` resource containing the resources populated from templates in the Questionnaire, derived from values in the QuestionnaireResponse. **Request**: Given a `QuestionnaireResponse` with an associated `Questionnaire`, e.g.` ```json { "resourceType": "QuestionnaireResponse", "id": "1c503f4e-a08c-4b7d-8ebd-bfc67b0ab761", "status": "completed", "questionnaire": "Questionnaire/f80e508b-9aaa-41e3-aa8e-0e4817c86b44" // ... } ``` ```http GET https://api.medplum.com/fhir/R4/QuestionnaireResponse/1c503f4e-a08c-4b7d-8ebd-bfc67b0ab761/$extract ``` ```bash curl 'https://api.medplum.com/fhir/R4/QuestionnaireResponse/1c503f4e-a08c-4b7d-8ebd-bfc67b0ab761/$extract' \ -H "Authorization: Bearer $MY_ACCESS_TOKEN" ``` **Response** (200 OK): ```json { "resourceType": "Bundle", "type": "transaction", "entry": [ // Populated template resources ] } ``` ### Related - [Parsing Questionnaire Responses](/docs/questionnaires/parsing-questionnaire-responses) — Complete guide to SDC workflows - [FHIR Questionnaire Resource](https://hl7.org/fhir/R4/questionnaire.html) - FHIR specification for Questionnaire - [FHIR QuestionnaireResponse Resource](https://hl7.org/fhir/R4/questionnaireresponse.html) - FHIR specification for QuestionnaireResponse - [HL7 SDC Implementation Guide](https://build.fhir.org/ig/HL7/sdc/) - Official Structured Data Capture IG - [Medplum Questionnaires Guide](/docs/questionnaires) - Building and using questionnaires in Medplum --- ## GraphQL The FHIR GraphQL API allows you to query and retrieve FHIR resources using GraphQL syntax, enabling precise data fetching with a single request. Unlike traditional REST endpoints that return fixed resource structures, GraphQL lets clients specify exactly which fields and related resources they need, reducing over-fetching and minimizing network round trips. **Use Cases:** - **Efficient data loading**: Fetch a patient along with their conditions, medications, and recent encounters in a single query instead of multiple REST calls - **Mobile and web applications**: Minimize bandwidth usage by requesting only the fields needed for specific UI components - **Complex data relationships**: Navigate FHIR references and retrieve nested resources (e.g., Patient → Encounters → Practitioners) in one request - **API exploration**: Use GraphQL introspection to discover available FHIR resources and their fields programmatically Medplum provides a GraphQL API based on the [FHIR GraphQL](https://hl7.org/fhir/graphql.html) draft specification. Check out our [FHIR GraphQL Guide](/docs/graphql) for an in depth explanation of how to leverage the FHIR GraphQL API. ### Related - [Medplum FHIR GraphQL Guide](/docs/graphql) - Comprehensive guide to using GraphQL with Medplum - [GraphQL Queries](/docs/graphql/mutations) - GraphQL mutations and write operations - [FHIR Search](/docs/search) - Alternative REST-based search capabilities - [FHIR GraphQL Specification](https://hl7.org/fhir/graphql.html) - Official FHIR GraphQL draft specification - [GraphQL Foundation](https://graphql.org/learn/) - GraphQL language documentation --- ## FHIR Operation Framework import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import ExampleCode from '!!raw-loader!@site/../examples/src/api/fhir/operations/operation-framework.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; In addition to the standard API endpoints for creating, reading, and updating resources, FHIR offers a variety of RPC-like [Operation APIs](https://www.hl7.org/fhir/operations.html) to expose arbitrary functionality as a FHIR API. Each API endpoint is defined by an [`OperationDefinition`](http://hl7.org/fhir/R4/operationdefinition.html) resource, which provides information about how to call the API and what to expect in response. ### Operation Definition For example, consider the [`ValueSet/$validate-code` operation](https://www.hl7.org/fhir/R4/valueset-operation-validate-code.html), which is summarized below:
OperationDefinition JSON ```js { "resourceType": "OperationDefinition", "url": "http://hl7.org/fhir/OperationDefinition/ValueSet-validate-code", "status": "active", "kind": "operation", // Endpoint configuration "code": "validate-code", "resource": ["ValueSet"], "system": false, "type": true, "instance": true, "parameter": [ // Input Parameters { "use": "in", "name": "url", "documentation": "Value set canonical URL", "min": 0, "max": "1", "type": "uri" }, { "use": "in", "name": "valueSet", "documentation": "The value set is provided directly as part of the request", "min": 0, "max": "1", "type": "ValueSet" }, { "use": "in", "name": "code", "documentation": "The code that is to be validated", "min": 0, "max": "1", "type": "code" }, { "use": "in", "name": "system", "documentation": "The system for the code that is to be validated", "min": 0, "max": "1", "type": "uri" }, { "use": "in", "name": "display", "documentation": "The display associated with the code, if provided", "min": 0, "max": "1", "type": "string" }, { "use": "in", "name": "coding", "documentation": "A coding to validate", "min": 0, "max": "1", "type": "Coding" }, { "use": "in", "name": "codeableConcept", "documentation": "A full codeableConcept to validate", "min": 0, "max": "1", "type": "CodeableConcept" }, // Output Parameters { "use": "out", "name": "result", "documentation": "True if the concept details supplied are valid", "min": 1, "max": "1", "type": "boolean" }, { "use": "out", "name": "message", "documentation": "Error details, if result = false; otherwise may contain hints and warnings", "min": 0, "max": "1", "type": "string" }, { "use": "out", "name": "display", "documentation": "A valid display for the concept if the system wishes to display this to a user", "min": 0, "max": "1", "type": "string" } ] } ```
This definition supplies the information needed to correctly call the operation API endpoint: - All operation requests can be made using the `POST` HTTP method - The `validate-code` operation is available as type- and instance-level endpoints for `ValueSet` resources, i.e. - `[baseUrl]/ValueSet/$validate-code` - `[baseUrl]/ValueSet/[id]/$validate-code` - Input parameters are passed via a [`Parameters`](/docs/api/fhir/resources/parameters) resource in the request body ### Invoking an Operation Given the information from the `OperationDefinition`, we can construct a request to the operation API endpoint. For each `in` parameter, corresponding entries may appear in the request `Parameters.parameter` array. Value types must match the `OperationDefinition`. **Request**: **TypeScript** {ExampleCode} **cURL** ```bash curl 'https://api.medplum.com/fhir/R4/ValueSet/$validate-code' \ -X POST \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer $MY_ACCESS_TOKEN" \ -d '{"resourceType":"Parameters","parameter":[{"name":"url","valueUri":"http://hl7.org/fhir/ValueSet/condition-severity"},{"name":"coding","valueCoding":{"system":"http://snomed.info/sct","code":"255604002"}}]}' ``` **Response**: (200 OK) ```js { "resourceType": "Parameters", "parameter": [ { "name": "result", "valueBoolean": true }, { "name": "display", "valueString": "Mild (qualifier value)" } ] } ``` #### Via GET Request In some cases, it may be simpler to invoke an operation with a GET request and encode the input parameters in the query string of the request URL. For example, the following operation requests are equivalent: ```bash curl 'https://api.medplum.com/fhir/R4/ValueSet/$validate-code' \ -X POST \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer $MY_ACCESS_TOKEN" \ -d '{"resourceType":"Parameters","parameter":[{"name":"url","valueUri":"http://hl7.org/fhir/ValueSet/condition-severity"},{"name":"system","valueUri":"http://snomed.info/sct"},{"name":"code","valueCode":"255604002"}]}' curl 'https://api.medplum.com/fhir/R4/ValueSet/$validate-code' \ --get \ -H "Authorization: Bearer $MY_ACCESS_TOKEN" \ -d 'url=http://hl7.org/fhir/ValueSet/condition-severity' \ -d 'system=http://snomed.info/sct' \ -d 'code=255604002' ``` This is possible when the request is idempotent and contains only simple input parameter types, i.e. - The `OperationDefinition` must not contain `"affectsState": true` - The request may only contain `in` parameters with a simple `type` (one starting with a lower-case letter, like `string` or `positiveInt` - but not `Coding`) ### Reading the Response For each `out` parameter in the response, the typed value(s) are recorded in the `Parameters.parameter` array. Multiple values for a given output parameter will appear as multiple entries in the array, **not** a nested array in the `value[x]` field. :::info[Returning Resources] Many operations return a specific resource type, instead of the usual `Parameters` resource. These cases are marked by a single, required `return` output parameter with `type` specifying the returned resource type. ::: #### Error Responses In case of an error, the server will return an HTTP status code in the 4xx-5xx range. The response body will contain an `OperationOutcome` resource with details about the error. For example, if the specified `ValueSet` could not be found by URL: ```js { "resourceType": "OperationOutcome", "issue": [ { "severity": "error" "code": "invalid", "details": { "text": "ValueSet http://example.com/ValueSet/missing not found" }, } ] } ``` ### Operation Documentation Details about the FHIR Operations supported by Medplum server are organized below by category. For information about other available operations, see the [complete list](https://www.hl7.org/fhir/R4/operationslist.html) from the FHIR specification. #### Terminology Operations Operations for managing CodeSystem, ConceptMap, and ValueSet resources. **CodeSystem:** - [CodeSystem/$import](/docs/api/fhir/operations/codesystem-import) - Import codes into a CodeSystem - [CodeSystem/$lookup](/docs/api/fhir/operations/codesystem-lookup) - Look up code details - [CodeSystem/$subsumes](/docs/api/fhir/operations/codesystem-subsumes) - Test subsumption relationship - [CodeSystem/$validate-code](/docs/api/fhir/operations/codesystem-validate-code) - Validate a code in a CodeSystem **ConceptMap:** - [ConceptMap/$import](/docs/api/fhir/operations/conceptmap-import) - Import mappings into a ConceptMap - [ConceptMap/$translate](/docs/api/fhir/operations/conceptmap-translate) - Translate a code using a ConceptMap **ValueSet:** - [ValueSet/$expand](/docs/api/fhir/operations/valueset-expand) - Expand a ValueSet - [ValueSet/$validate-code](/docs/api/fhir/operations/valueset-validate-code) - Validate a code against a ValueSet #### Patient Operations Operations specific to Patient resources. - [Patient/$everything](/docs/api/fhir/operations/patient-everything) - Retrieve all patient data - [Patient/$summary](/docs/api/fhir/operations/patient-summary) - Generate International Patient Summary (IPS) #### Bot Operations Deploy, execute, and extend Medplum Bots. - [Bot/$deploy](/docs/api/fhir/operations/bot-deploy) - Deploy bot code - [Bot/$execute](/docs/api/fhir/operations/bot-execute) - Execute a bot - [Custom Bot Operations](/docs/api/fhir/operations/custom-operations) - Create custom FHIR operations with Bots #### Resource Validation & Transformation Validate resources and transform data structures. - [$graph](/docs/api/fhir/operations/resource-graph) - Fetch related resources via GraphDefinition - [GraphQL](/docs/api/fhir/operations/graphql) - Query resources using GraphQL - [Questionnaire/$extract](/docs/api/fhir/operations/extract) - Extract data from QuestionnaireResponse - [Resource/$validate](/docs/api/fhir/operations/validate-a-resource) - Validate a resource against profiles - [StructureDefinition/$expand-profile](/docs/api/fhir/operations/structuredefinition-expand-profile) - Expand a StructureDefinition with nested profiles #### Data Export & Import Bulk data operations and format conversions. - [$csv](/docs/api/fhir/operations/csv) - Export resources as CSV - [Bulk Data Export](/docs/api/fhir/operations/bulk-fhir) - FHIR Bulk Data Access - [CCDA Export](/docs/api/fhir/operations/ccda-export) - Export data as C-CDA documents - [Claim/$export](/docs/api/fhir/operations/claim-export) - Export claims as CMS-1500 PDFs #### Clinical Decision Support Operations for measures, clinical plans, charges, and AI assistance. - [AI Assistant](/docs/api/fhir/operations/ai) - AI-powered clinical assistance - [ChargeItemDefinition/$apply](/docs/api/fhir/operations/chargeitemdefinition-apply) - Apply pricing rules to ChargeItem - [Measure/$evaluate-measure](/docs/api/fhir/operations/evaluate-measure) - Evaluate a clinical quality measure - [PlanDefinition/$apply](/docs/api/fhir/operations/plandefinition-apply) - Apply a PlanDefinition to generate resources #### Project & System Administration Project management and system operations. - [AsyncJob/$cancel](/docs/api/fhir/operations/asyncjob-cancel) - Cancel an asynchronous job - [Project/$clone](/docs/api/fhir/operations/project-clone) - Clone a Medplum project - [Project/$init](/docs/api/fhir/operations/project-init) - Initialize a new project - [Project/$rate-limits](/docs/api/fhir/operations/project-rate-limits) - View FHIR interaction quota usage - [$expunge](/docs/api/fhir/operations/expunge) - Permanently delete resources - [$resend](/docs/api/fhir/operations/resend) - Resend a subscription notification - [$set-accounts](/docs/api/fhir/operations/set-accounts) - Set resource account references #### Authentication & Security Client application, credential, and user management operations. - [ClientApplication/$rotate-secret](/docs/api/fhir/operations/rotate-client-secret) - Rotate client secrets - [ClientApplication/$smart-launch](/docs/api/fhir/operations/clientapplication-smart-launch) - SMART on FHIR app launch - [User/$rescope](/docs/api/fhir/operations/user-rescope) - Move a user between server and project scope - [User/$update-email](/docs/api/fhir/operations/user-update-email) - Update user email address --- ## Patient $everything The `$everything` operation retrieves a complete bundle of all resources associated with a patient in a single API call. This provides a comprehensive view of a patient's clinical record-including encounters, observations, medications, conditions, and related resources-without requiring multiple queries. This operation is essential for scenarios requiring complete patient data, such as care transitions, patient record requests, and regulatory compliance with data portability requirements. ### Use Cases - **Patient Record Requests**: Fulfill patient requests for their complete medical record (21st Century Cures Act compliance) - **Care Transitions**: Grabbing a patient's full history when transferring care to another provider - **Analytics and Research**: Extract comprehensive patient data for population health analysis or research studies ### Invoke the `$everything` operation ``` [base]/R4/Patient//$everything ``` #### Parameters | Name | Type | Description | Required | | --------- | ------------ | ------------------------------------------------------------------------ | -------- | | `start` | `date` | Start date for filtering resources by care date | No | | `end` | `date` | End date for filtering resources by care date | No | | `_since` | `instant` | Only include resources updated since this time | No | | `_count` | `integer` | Maximum number of results to return (default: 1000) | No | | `_offset` | `integer` | Offset for pagination | No | | `_type` | `string[]` | Restrict output to specific resource types (comma-separated) | No | #### Output The output of the request is a [FHIR bundle](/docs/api/fhir/resources/bundle) with all resources belonging to the patient for whom the request is made. The FHIR Bundle created from this operation is the supported machine readable Electronic Health Information Export (EHI) format for Medplum. ##### Resources Included Medplum's patient `$everything` operation includes: 1. **Patient Compartment Resources** - All resources defined in the [FHIR R4 Patient Compartment](https://hl7.org/fhir/R4/compartmentdefinition-patient.html) 2. **Referenced Resources** - Additional resources referenced by patient compartment resources but outside the compartment itself: - Organizations - Practitioners - PractitionerRoles - Locations - Medications - Devices ### Related - [MedplumClient.readPatientEverything](/docs/sdk/core.medplumclient.readpatienteverything) - TypeScript SDK method - [Patient $ccda-export](/docs/api/fhir/operations/ccda-export) - Export patient data as C-CDA document - [ONC Certification](/docs/compliance/onc) - Compliance requirements for patient data export - [FHIR Patient Compartment](https://hl7.org/fhir/R4/compartmentdefinition-patient.html) - FHIR specification for patient-related resources - [FHIR $everything Operation](https://hl7.org/fhir/R4/patient-operation-everything.html) - FHIR specification for $everything --- ## Patient $match The `$match` operation implements [Master Patient Index (MPI)](https://en.wikipedia.org/wiki/Master_patient_index) patient matching. It accepts a (possibly partial) Patient resource, searches your project for candidates, and returns a `Bundle` of matches — each annotated with a score and match grade. Medplum's matching is based on the **CMS Patient Matching framework**, an evidence-based model that defines a fixed set of identity-attribute combinations strong enough to identify a patient. Rather than an ad-hoc weighted heuristic, matching is grounded in these approved combinations, with consistent normalization and conservative, constrained fuzzy matching. :::note[CMS guidelines are in draft] The CMS Patient Matching framework is a draft proposal and is **subject to change as the guidelines evolve**. Medplum implements the v3.4.0 Final Consolidated Draft. The criteria table and behavior described here may be updated in future releases. ::: ### Use Cases - **Duplicate Prevention**: Check for existing patients before registering a new one to avoid creating duplicate records. - **Patient Reconciliation**: Match incoming demographics from an external system against records in your Medplum project. - **Identity Resolution / Discovery**: Find candidate patients when only partial demographics are known (human-reviewed search). - **Record Disclosure**: Determine whether incoming demographics unambiguously identify exactly one patient before returning their record (machine-to-machine exchange). ### Invocation ``` POST [base]/Patient/$match ``` ### Input Parameters | Parameter | Cardinality | Type | Description | | -------------------- | ----------- | --------- | ----------------------------------------------------------------------------------------------------------------------------- | | `resource` | 1..1 | `Patient` | The patient to match against (may be partial). Must include at least one of: `identifier`, `name`, `birthDate`, or `telecom`. | | `onlyCertainMatches` | 0..1 | `boolean` | Selects the matching mode. See [Matching Modes](#matching-modes). Defaults to `false`. | | `count` | 0..1 | `integer` | Maximum number of results (discovery mode only). Defaults to the server's default search count. | ### Matching Modes The same candidate-gathering and field-comparison pipeline runs in both modes; `onlyCertainMatches` changes the release rules. #### Discovery (`onlyCertainMatches: false`, default) Returns a ranked, graded list from the gathered candidate set for human review or triage. Approved CMS combination matches are ranked first with score `1.0`; other candidates receive a lightweight FHIR `$match` discovery score below `1.0`. Results are ordered from most to least likely and limited by `count`. #### Disclosure (`onlyCertainMatches: true`) Applies a **uniqueness gate**: the operation returns a patient **only if exactly one** candidate is a `certain` match (i.e. satisfies an approved CMS combination). If no candidate qualifies, if **two or more** distinct candidates qualify (an ambiguous result), or if candidate search is truncated such that uniqueness cannot be proven, the bundle is empty. This is the conservative behavior appropriate for releasing records in cross-organization exchange, where a wrong-patient disclosure is a critical error. **Twin guardrail:** if more than one gathered candidate has the query's date of birth (within ±1 day, since twins can be born either side of midnight), First Name must match exactly for every candidate. This prevents a one-edit fuzzy match from conflating twins with similar names, such as `Jayden` and `Jaden`. ### Output Returns a `Bundle` of type `searchset`. Each entry contains a matched `Patient` with a `search` element: - `entry.search.score` — A score from `0` to `1`. See [Scoring](#scoring). - `entry.search.extension` — A [`match-grade`](https://hl7.org/fhir/R4/extension-match-grade.html) extension, plus Medplum CMS extensions when an approved combination is satisfied: | Extension URL | Value | Meaning | | -------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------- | | `http://hl7.org/fhir/StructureDefinition/match-grade` | `code` | The match grade (see below). | | `https://medplum.com/fhir/StructureDefinition/cms-match-combination` | `string` | The CMS combination ID that was satisfied (e.g. `"02"`). | | `https://medplum.com/fhir/StructureDefinition/cms-match-type` | `code` | `exact` or `fuzzy` — whether the satisfied CMS combination used fuzzy comparison. | ### Match Grades The `match-grade` separates strict CMS combination matches from lower-confidence discovery candidates: | Grade | Meaning | | --------------- | ----------------------------------------------------------------------------- | | `certain` | An approved CMS combination is fully satisfied. | | `probable` | A non-CMS discovery candidate with a score greater than or equal to 0.65. | | `possible` | A non-CMS discovery candidate with a score greater than or equal to 0.20. | | `certainly-not` | A candidate is explicitly blocked, such as by a generational-suffix conflict. | Candidates below the `possible` threshold, and candidates blocked by a generational-suffix conflict, are excluded from results. ### Scoring The `search.score` is intentionally **not** a probability — it is a simple, explainable ranking value: - **`1.0`** when an approved CMS combination is satisfied (`certain`). - In discovery mode only, otherwise **`min(x / 12, 0.9)`**, where `x` is the weighted count of the 12 identity factors that agree: an **exact** field counts as `1`, a **fuzzy** field as `0.5`. The `0.9` ceiling keeps any non-approved field set strictly below a real CMS match. This non-CMS score is Medplum's FHIR `$match` discovery ranking aid; it is not part of the CMS Table 2 release rule. Within a single query the denominator is constant, so candidates sort correctly by score; grade carries the human-facing classification. ### Identity Factors Twelve factors are used (gender is **not** a matching factor): First Name · Last Name · Date of Birth · Street Line · ZIP Code · Phone Number · Email Address · SSN (last 4) · ITIN (last 4) · MBI · Legal ID · Namespace-bound Unique Identifier Identifier factors are matched by their `system` (issuing-authority namespace) and value, using the FHIR token convention `system|value`. An identifier with a system outside the CMS-specific namespaces is treated as a **namespace-bound unique identifier** (for example EMPI, FHIR Patient Identifier, CSP UUID, or project MRN). ### Approved CMS Matching Combinations A candidate is a `certain` match when its agreeing factors form one of these approved combinations and it is the unique such candidate. Fields marked with `*` may be satisfied by a fuzzy comparison. For names and street lines, that means one edit (see [Fuzzy Matching](#fuzzy-matching)). For DOB, it means within ±1 day. **At most one** field per match may be fuzzy. Combination IDs follow v3.4.0 numbering, which differs from earlier drafts. Audit events record the specification version alongside the combination ID. | ID | Field Combination | | --- | -------------------------------------------------- | | 01 | First Name\* + Last Name\* + DOB\* + Street Line\* | | 02 | First Name + Last Name\* + DOB\* + Phone | | 03 | First Name\* + Last Name\* + DOB\* + Email | | 04 | First Name\* + Last Name + DOB + SSN (last 4) | | 05 | First Name + Last Name\* + DOB + SSN (last 4) | | 06 | First Name\* + Last Name + DOB + ITIN (last 4) | | 07 | First Name + Last Name\* + DOB + ITIN (last 4) | | 08 | First Name + DOB + MBI | | 09 | First Name + DOB + Legal ID | | 10 | Last Name\* + DOB\* + Legal ID | | 11 | First Name + DOB + Phone | | 12 | First Name + DOB + Email | | 13 | First Name + Phone + SSN (last 4) | | 14 | First Name + Phone + ITIN (last 4) | | 15 | First Name + Email + SSN (last 4) | | 16 | First Name + Email + ITIN (last 4) | | 17 | Phone + MBI | | 18 | Phone + Legal ID | | 19 | Email + MBI | | 20 | Email + Legal ID | | 21 | Legal ID + MBI | | 22 | Namespace-bound Unique Identifier | | 29 | First Name\* + Last Name\* + Phone + ZIP | | 30 | Last Name\* + DOB + Phone | Rules 23 to 28 use insurance Member ID or Subscriber ID within a payer namespace. They are **not evaluated**, because the specification does not yet define how a payer-namespaced identifier is conveyed in a `$match` query. Rule 29 has no DOB, so on its own it cannot tell apart a parent and child who share a name, phone, and ZIP. Medplum does **not** apply rule 29 when both records have a DOB and the DOBs disagree. ### Normalization Every field represents the set of **all known values** (current and historical — e.g. maiden/previous names, all addresses, all phone numbers), and matching compares against all of them. Before comparison, values are normalized: - **Strings** are matched case-insensitively, with whitespace and punctuation removed and diacritics folded (e.g. `José` → `jose`). - **Phone numbers** are normalized by stripping punctuation and whitespace. U.S. `+1` / leading-`1` eleven-digit numbers are normalized to the same ten digits as domestic notation. Other country codes are left as digits without additional international parsing. Phone numbers match regardless of type (home/cell/work). - **Email** is lowercased and trimmed. Punctuation is preserved, so `john.smith@example.com` and `johnsmith@example.com` remain distinct. - **Date of birth** must be a full `YYYY-MM-DD` date; partial dates are not imputed and are ignored for matching. - **ZIP code** uses the first 5 digits. - **SSN / ITIN** use only the last 4 folded alphanumeric characters. - A **generational-suffix conflict** (both records have a suffix and they disagree after folding, e.g. `Jr` vs `Sr`) blocks the match. Matching is intentionally limited to these mechanical, deterministic normalizations. Medplum ships **no** opinionated nickname equivalence table (`Bob`↔`Robert`) or placeholder/test-value suppression table, so behavior is transparent and reproducible. #### Fuzzy Matching Fuzzy matching is constrained: it applies only to **First Name, Last Name, and Street Line**, only where a combination permits it (`*` above), only to values at least **5 characters** long, and tolerates a **Damerau–Levenshtein distance of 1** (one insertion, deletion, substitution, or adjacent transposition). A starred **DOB** may instead be within **±1 day**. At most one field per match may be fuzzy, and a ±1 day DOB counts as that field. Phonetic matching (e.g. Soundex) is not used. ### Candidate Search Before scoring, candidates are gathered with selective FHIR searches anchored on the query's exact identifiers, telecom, and name + birthdate: - `Patient?identifier=|` for each identifier - `Patient?telecom=` for each phone or email - `Patient?birthdate=ge&birthdate=le&family=` and the same with `given=` when a birth date is present, covering the ±1 day DOB tolerance Results are deduplicated by patient ID, then compared and scored in memory. Discovery mode ranks the gathered candidate set; it is intended for review and is not an exhaustive population scan. In disclosure mode, if a search hits its result cap (uniqueness cannot be proven), the match is suppressed. ### Example #### Request ```http POST /fhir/R4/Patient/$match Content-Type: application/fhir+json { "resourceType": "Parameters", "parameter": [ { "name": "resource", "resource": { "resourceType": "Patient", "name": [{ "family": "Smith", "given": ["John"] }], "birthDate": "1970-03-15", "telecom": [{ "system": "phone", "value": "555-867-5309" }] } }, { "name": "onlyCertainMatches", "valueBoolean": false } ] } ``` #### Response ```json { "resourceType": "Bundle", "type": "searchset", "total": 1, "entry": [ { "resource": { "resourceType": "Patient", "id": "patient-abc", "name": [{ "family": "Smith", "given": ["John"] }], "birthDate": "1970-03-15", "telecom": [{ "system": "phone", "value": "5558675309" }] }, "search": { "mode": "match", "score": 1.0, "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/match-grade", "valueCode": "certain" }, { "url": "https://medplum.com/fhir/StructureDefinition/cms-match-combination", "valueString": "02" }, { "url": "https://medplum.com/fhir/StructureDefinition/cms-match-type", "valueCode": "exact" } ] } } ] } ``` Here First Name + Last Name + DOB + Phone match after U.S. phone normalization, satisfying combination `02` — a `certain` match with score `1.0`. In disclosure mode (`onlyCertainMatches: true`) this same single unique match would be returned; if a second patient also satisfied a combination, the bundle would instead be empty. ### Error Responses | Status Code | Description | | ----------------- | ------------------------------------------------------------------------------- | | `400 Bad Request` | `resource` parameter is missing or is not a Patient | | `400 Bad Request` | Input Patient has no matchable fields (identifier, name, birthDate, or telecom) | | `403 Forbidden` | Insufficient permissions to search Patient resources | ### Related Documentation - [HL7 FHIR Patient $match operation](https://hl7.org/fhir/R4/patient-operation-match.html) - [Patient $everything](./patient-everything) - [Patient $summary](./patient-summary) --- ## Patient $summary The `$summary` operation generates an International Patient Summary (IPS) document for a patient. This creates a comprehensive clinical summary following the [IPS Implementation Guide](https://hl7.org/fhir/uv/ips/). ### Use Cases - **Care Transitions and Referrals**: Generate standardized clinical summaries when patients move between care settings or providers - **Patient Portal Health Summaries**: Provide patients with comprehensive, readable summaries of their health record - **Emergency Department Quick Views**: Give ED clinicians rapid access to essential patient information during emergencies - **Cross-Border Health Record Sharing**: Share patient data internationally using the standardized IPS format ### Invocation ``` GET [base]/Patient/[id]/$summary POST [base]/Patient/[id]/$summary ``` ### Input Parameters | Parameter | Cardinality | Type | Description | |-----------|-------------|------|-------------| | `author` | 0..1 | `Reference` | Reference to Practitioner, PractitionerRole, or Organization to list as the composition author. Defaults to the current user. | | `authoredOn` | 0..1 | `instant` | The date/time the summary was authored. Defaults to current time. | | `start` | 0..1 | `date` | Start date for filtering resources | | `end` | 0..1 | `date` | End date for filtering resources | | `_since` | 0..1 | `instant` | Only include resources modified since this date | | `identifier` | 0..1 | `string` | Identifier for the composition | | `profile` | 0..1 | `canonical` | Profile to validate against | ### Output Returns a `Bundle` of type `document` containing: 1. **Composition** - The IPS Composition resource organizing all sections 2. **Patient** - The patient resource 3. **Author** - The author (Practitioner/PractitionerRole/Organization) 4. **Clinical Resources** - All relevant clinical data organized into sections ### IPS Sections The summary includes the following sections (when data is available): | Section | LOINC Code | Resources | |---------|------------|-----------| | Allergies | 48765-2 | AllergyIntolerance | | Immunizations | 11369-6 | Immunization | | Medications | 10160-0 | MedicationRequest | | Problem List | 11450-4 | Condition | | Results | 30954-2 | DiagnosticReport, Observation | | Social History | 29762-2 | Observation (social-history category) | | Vital Signs | 8716-3 | Observation (vital-signs category) | | Procedures | 47519-4 | Procedure | | Encounters | 46240-8 | Encounter | | Devices | 46264-8 | DeviceUseStatement | | Assessments | 51848-0 | ClinicalImpression | | Plan of Treatment | 18776-5 | CarePlan, Goal, ServiceRequest | | Goals | 61146-7 | Goal | | Health Concerns | 75310-3 | Condition (health-concern category) | | Functional Status | 47420-5 | Observation (survey category) | | Notes | 34109-9 | ClinicalImpression (note type) | | Reason for Referral | 42349-1 | ServiceRequest (referral type) | | Insurance | 48768-6 | Account | ### Example #### Request ```http GET /fhir/R4/Patient/patient123/$summary ``` #### Response ```json { "resourceType": "Bundle", "type": "document", "timestamp": "2026-01-08T15:30:00.000Z", "entry": [ { "resource": { "resourceType": "Composition", "id": "composition123", "status": "final", "type": { "coding": [ { "system": "http://loinc.org", "code": "60591-5", "display": "Patient Summary" } ] }, "subject": { "reference": "Patient/patient123" }, "date": "2026-01-08T15:30:00.000Z", "author": [ { "reference": "Practitioner/practitioner456" } ], "title": "Medical Summary", "section": [ { "title": "Allergies", "code": { "coding": [ { "system": "http://loinc.org", "code": "48765-2" } ] }, "text": { "status": "generated", "div": "
...
" }, "entry": [ { "reference": "AllergyIntolerance/allergy789" } ] } ] } }, { "resource": { "resourceType": "Patient", "id": "patient123" } }, { "resource": { "resourceType": "AllergyIntolerance", "id": "allergy789" } } ] } ``` ### Resource Types Included The operation collects and organizes the following resource types: - Account - AllergyIntolerance - CarePlan - ClinicalImpression - Condition - Coverage - DeviceUseStatement - DiagnosticReport - Encounter - Goal - Immunization - MedicationRequest - Observation - Procedure - RelatedPerson - ServiceRequest ### Behavior 1. **Data Collection**: Uses the `$everything` operation internally to gather all patient-related resources 2. **Section Assignment**: Automatically assigns resources to appropriate IPS sections based on resource type and category codes 3. **Nested Resources**: Handles nested resources (e.g., Observations that are members of other Observations) appropriately 4. **HTML Generation**: Generates human-readable HTML tables for each section 5. **Event Period**: Calculates the composition event period from resource timestamps ### Error Responses | Status Code | Description | |-------------|-------------| | `404 Not Found` | Patient not found | | `403 Forbidden` | Insufficient permissions | ### Related Documentation - [International Patient Summary IG](https://hl7.org/fhir/uv/ips/) - [Patient $everything](./patient-everything) - [Composition Resource](https://www.hl7.org/fhir/composition.html) --- ## PlanDefinition $apply Medplum implements the FHIR [`$apply` operation](https://hl7.org/fhir/plandefinition-operation-apply.html) for PlanDefinition resources. The `$apply` operation converts a PlanDefinition into a set of actionable resources for a specific patient. It creates a CarePlan containing a RequestGroup with Tasks based on the actions defined in the PlanDefinition. For a detailed explanation of how Medplum processes PlanDefinitions, resolves ActivityDefinitions, and handles custom Task extensions, see our guide on [Authoring Clinical Protocols](/docs/careplans/protocols). ### Use Cases - **Care Protocols**: Apply standardized care protocols to patients - **Clinical Guidelines**: Implement evidence-based treatment guidelines - **Care Coordination**: Generate care plans with assigned tasks - **Workflow Automation**: Create structured workflows from templates ### Invoke the `$apply` operation ``` [base]/PlanDefinition/[id]/$apply ``` For example: ```bash curl -X POST 'https://api.medplum.com/fhir/R4/PlanDefinition/example-id/$apply' \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "subject", "valueString": "Patient/patient-123" } ] }' ``` ### Parameters | Name | Type | Description | Required | | -------------- | ----------- | --------------------------------------------------- | -------- | | `subject` | `string[]` | Reference to the Patient the plan applies to | Yes | | `encounter` | `string` | Reference to the Encounter for context | No | | `practitioner` | `string` | Reference to the Practitioner applying the plan | No | | `organization` | `string` | Reference to the Organization context | No | #### Example with All Parameters ```bash curl -X POST 'https://api.medplum.com/fhir/R4/PlanDefinition/example-id/$apply' \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "subject", "valueString": "Patient/patient-123" }, { "name": "encounter", "valueString": "Encounter/encounter-456" }, { "name": "practitioner", "valueString": "Practitioner/dr-smith" }, { "name": "organization", "valueString": "Organization/clinic-abc" } ] }' ``` ### Output The operation returns a CarePlan resource that references a RequestGroup containing the generated Tasks. #### Example Response ```json { "resourceType": "CarePlan", "id": "generated-careplan-id", "status": "active", "intent": "plan", "subject": { "reference": "Patient/patient-123" }, "instantiatesCanonical": [ "http://example.org/PlanDefinition/diabetes-management" ], "activity": [ { "reference": { "reference": "RequestGroup/generated-requestgroup-id" } } ] } ``` ### Example PlanDefinition At its most basic level, a `PlanDefinition` is a list of sequential tasks. The `$apply` operation converts these actions into a `RequestGroup` and individual `Task` resources. ```json { "resourceType": "PlanDefinition", "id": "example-plan", "url": "http://example.org/PlanDefinition/diabetes-screening", "title": "Diabetes Screening Protocol", "status": "active", "action": [ { "title": "Order HbA1c Test", "definitionCanonical": "http://example.org/ActivityDefinition/hba1c-order" }, { "title": "Complete Risk Assessment", "definitionCanonical": "http://example.org/Questionnaire/diabetes-risk" }, { "title": "Schedule Follow-up", "description": "Schedule a follow-up appointment in 3 months" } ] } ``` For more advanced examples of modeling workflows (including hierarchical actions, conditional logic, timing dependencies, and dynamic values), see our guide on [Authoring Clinical Protocols](/docs/careplans/protocols). ### Related Documentation - [Authoring Clinical Protocols](/docs/careplans/protocols) - Advanced workflow modeling with PlanDefinition - [CarePlans](/docs/careplans) - Working with care plans in Medplum - [Task Management](/docs/careplans/tasks) - Managing tasks and workflows - [FHIR PlanDefinition](https://hl7.org/fhir/plandefinition.html) - FHIR specification --- ## Project $clone The `$clone` operation creates a complete copy of a Medplum project, including all its resources. This is useful for creating test environments, project templates, or migrating projects. ### Use Cases - **Creating a Test Environment**: Clone production to create isolated testing environments - **Creating a Project Template**: Clone configuration and bots without patient data for new client projects - **Cloning Specific Resources**: Selectively clone bots, questionnaires, or other resources :::caution[Self-Hosted Deployments Only] This operation requires super admin privileges, which are only available on self-hosted Medplum deployments. If you are using Medplum's cloud-hosted service and need to clone a project, please contact [Medplum support](/contact). ::: ### Invocation ``` POST [base]/Project/[id]/$clone ``` ### Input Parameters Parameters are passed in the request body as a JSON object (not as FHIR Parameters): | Parameter | Type | Description | |-----------|------|-------------| | `name` | `string` | (Optional) Name for the new project. If provided, updates the project name and related resources. | | `resourceTypes` | `string[]` | (Optional) Array of resource types to include. If empty, all resource types are cloned. | | `includeIds` | `string[]` | (Optional) Array of specific resource IDs to include. If empty, all resources are included. | | `excludeIds` | `string[]` | (Optional) Array of resource IDs to exclude from cloning. | ### Output Returns the newly created `Project` resource. ### Behavior 1. **Resource Discovery**: Scans all resource types in the source project (up to 1000 resources per type) 2. **ID Mapping**: Generates new IDs for all resources while maintaining internal references 3. **Reference Rewriting**: Updates all internal references to point to new resource IDs 4. **Binary Copying**: Copies Binary resources and their associated storage files 5. **Project Creation**: Creates the new project first, then all dependent resources 6. **Name Updates**: If a name is provided, updates: - Project name - ProjectMembership display names - Default ClientApplication names ### Example #### Request ```http POST /fhir/R4/Project/source-project-id/$clone Content-Type: application/json { "name": "My Cloned Project", "resourceTypes": ["Patient", "Practitioner", "Organization", "Bot"], "excludeIds": ["test-data-id-1", "test-data-id-2"] } ``` #### Response ```json { "resourceType": "Project", "id": "new-project-id", "name": "My Cloned Project", "owner": { "reference": "User/user123" } } ``` ### What Gets Cloned The following resources are cloned (if `resourceTypes` is not specified): - All FHIR resources in the project - ProjectMemberships - ClientApplications - Bots (including code) - Binary resources (including stored files) - Access policies - User configurations ### What Does NOT Get Cloned - Login history - Audit logs - AsyncJob resources - External system connections (must be reconfigured) ### Example Configurations #### Creating a Test Environment ```json { "name": "Production Copy - Testing", "excludeIds": ["sensitive-patient-1", "sensitive-patient-2"] } ``` #### Creating a Project Template ```json { "name": "New Client Project", "resourceTypes": ["Bot", "StructureDefinition", "ValueSet", "Questionnaire"] } ``` #### Cloning Specific Resources ```json { "includeIds": ["bot-1", "bot-2", "questionnaire-1"] } ``` ### Error Responses | Status Code | Description | |-------------|-------------| | `403 Forbidden` | User is not a super admin (requires self-hosted deployment) | | `404 Not Found` | Source project not found | ### Notes - The operation may take some time for large projects - Binary files are copied asynchronously - The new project will have a new owner (the user performing the clone) - Secrets and credentials in Bots are not copied and must be reconfigured ### Related Documentation - [Projects](/docs/access/projects) - [Project Resource](/docs/api/fhir/medplum/project) --- ## Project $init The `$init` operation creates a new Medplum Project with an admin user in a single API call. Projects provide isolated environments for different applications, organizations, or tenants-each with their own users, access policies, and FHIR resources. This operation streamlines the onboarding process for multi-tenant platforms, allowing you to programmatically provision new customer environments without manual setup steps. ### Use Cases - **Customer Onboarding**: Automatically create isolated project environments when new customers sign up - **Development Environments**: Spin up separate projects for development, staging, and production - **Multi-Tenant SaaS**: Provision tenant-specific projects with dedicated resources and access controls - **Testing and QA**: Create temporary projects for automated testing scenarios - **Partner Integrations**: Set up isolated environments for third-party integrations ### Invoke the `$init` operation ``` [base]/Project/$init ``` For example: ```bash curl 'https://api.medplum.com/fhir/R4/Project/$init' \ -X POST \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -d '{"resourceType":"Parameters", "parameter":[ {"name":"name", "valueString":"Test Project"} ]}' ``` #### Success Response Example outcome: ```json { "resourceType": "Project", "id": "59aa86af-9673-4862-a441-5200e6163bfb", "name": "Test Project", "strictMode": true, "owner": { "reference": "User/eb0f26b9-1e42-4886-85fc-31919aeebc55" } } ``` #### Error Response Example outcome when Project name not provided: ```json { "resourceType": "OperationOutcome", "id": "3820fda7-c6d4-4207-b597-90631f0881f9", "issue": [ { "severity": "error", "code": "invalid", "details": { "text": "Expected required input parameter 'name'" }, "expression": ["Parameters.parameter"] } ] } ``` ### Assigning Project Ownership By default, the API will attempt to use the `User` associated with the current access token as the Project `owner`. In cases where a different user should be the owner, or when the access token is not associated with a `User`, one can be manually assigned using a parameter: either `owner` for existing users, or `ownerEmail` to create the user. ```bash curl 'https://api.medplum.com/fhir/R4/Project/$init' \ -X POST \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -d '{"resourceType":"Parameters", "parameter":[ {"name":"name", "valueString":"Test Project"}, \ {"name":"owner", "valueReference": {"reference": "User/cb5bb36b-fcfa-4c07-8da1-5f3afd8d261d" } } ]}' ``` ### Related - [Projects Guide](/docs/access/projects) - Understanding Medplum projects and multi-tenancy - [Access Control](/docs/access/access-policies) - Configuring access policies for projects - [Admin Access](/docs/access/admin) - Administrative access and operations - [User Configuration](/docs/access/user-configuration) - Managing users and invitations - [Medplum Project Resource](/docs/api/fhir/medplum/project) - Project resource reference --- ## Project $rate-limits The `$rate-limits` operation returns a snapshot of FHIR interaction quota usage for a project: consumed points, remaining points, configured limits, and time until reset. Use it to monitor load across the project and per membership (users, bots, and client applications). :::note[Admin required] Requires [project admin](/docs/access/admin) or super admin access. ::: ### Medplum App In the Medplum App, open **Project Admin** → **Rate Limits** ([`/admin/rate-limits`](https://app.medplum.com/admin/rate-limits)) and click **Refresh** to load the latest data. ### Invoke the `$rate-limits` operation ``` GET [base]/Project/{projectId}/$rate-limits ``` #### Input parameters | Parameter | Description | | -------------- | --------------------------------------------------------------------------------------------- | | `membershipId` | One or more `ProjectMembership` IDs. When omitted, active consumers are returned (see below). | import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; **TypeScript** ```typescript const params = await medplum.get(medplum.fhirUrl('Project', projectId, '$rate-limits')); // Optional: filter by membership // const url = medplum.fhirUrl('Project', projectId, '$rate-limits'); // url.searchParams.append('membershipId', 'f47ac10b-58cc-4372-a567-0e02b2c3d479'); // const params = await medplum.get(url); ``` **cURL** ```bash curl 'https://api.medplum.com/fhir/R4/Project/{projectId}/$rate-limits' \ -H 'Authorization: Bearer MY_ACCESS_TOKEN' # Optional: filter by membership curl -G 'https://api.medplum.com/fhir/R4/Project/{projectId}/$rate-limits' \ -H 'Authorization: Bearer MY_ACCESS_TOKEN' \ --data-urlencode 'membershipId=f47ac10b-58cc-4372-a567-0e02b2c3d479' ``` #### Example response ```json { "resourceType": "Parameters", "parameter": [ { "name": "project", "part": [ { "name": "id", "valueString": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, { "name": "limit", "valueInteger": 500000 }, { "name": "consumedPoints", "valueInteger": 150000 }, { "name": "remainingPoints", "valueInteger": 350000 }, { "name": "msBeforeReset", "valueInteger": 45000 } ] }, { "name": "membership", "part": [ { "name": "membershipId", "valueString": "f47ac10b-58cc-4372-a567-0e02b2c3d479" }, { "name": "profile", "valueReference": { "reference": "Practitioner/abc123", "display": "Dr. Alice Smith" } }, { "name": "limit", "valueInteger": 50000 }, { "name": "consumedPoints", "valueInteger": 45000 }, { "name": "remainingPoints", "valueInteger": 5000 }, { "name": "msBeforeReset", "valueInteger": 30000 } ] }, { "name": "membership", "part": [ { "name": "membershipId", "valueString": "6ba7b810-9dad-11d1-80b4-00c04fd430c8" }, { "name": "profile", "valueReference": { "reference": "ClientApplication/bot-sync", "display": "Sync Bot" } }, { "name": "limit", "valueInteger": 50000 }, { "name": "consumedPoints", "valueInteger": 10000 }, { "name": "remainingPoints", "valueInteger": 40000 }, { "name": "msBeforeReset", "valueInteger": 55000 } ] } ] } ``` ### Behavior When no `membershipId` is specified, the server returns memberships that have had recent FHIR activity (up to 1,000 active consumers). Members with no consumption in the current window may be omitted. When `membershipId` is specified, only those memberships are included (useful for bots or service accounts). Quota fields are omitted when a counter has not been used in the current window. Membership `limit` values use each membership's effective quota: a `UserConfiguration` `fhirQuota` override when present, otherwise the project `userFhirQuota` (or the server default). The project `limit` uses `totalFhirQuota` when set, otherwise the project default user quota × 10. See [user-specific FHIR quotas](/docs/access/user-configuration#user-specific-fhir-quota-rate-limits). :::note[Independent reset windows] FHIR quota counters are stored in Redis with a **separate 60-second window for each key**: one for the project total and one per membership. Each window starts on that key's **first** consumption in the period; later requests add points but do not extend the window. Because the project key is shared across all memberships, its window may have started earlier than a given membership's window (for example, after another bot or user made a request). At a single point in time you may see a membership with a **high** `consumedPoints` value near the end of its window while the **project** total is **low** or has just reset, with different `msBeforeReset` values. That is expected when windows are out of phase, and not a sign that the project total failed to sum membership usage. When comparing project and membership utilization, check `msBeforeReset` on each row. Aligned reset times are a better signal that the counters describe the same window. ::: ### Related - [Rate Limits](/docs/rate-limits) — FHIR interaction quotas, point weights, and configuration - [User Configuration](/docs/access/user-configuration#user-specific-fhir-quota-rate-limits) — Per-user FHIR quota overrides - [Project settings](/docs/self-hosting/project-settings) — `userFhirQuota` and `totalFhirQuota` system settings for self-hosters --- ## Refresh Reference Display Strings The `$refresh-reference-display` operation updates the `Reference.display` strings within a resource to reflect the current display string for the referenced resource. This is used to update resources when e.g. linked Patient names are changed. ### Invocation ``` POST [base]/[ResourceType]/[id]/$refresh-reference-display ``` ### Input Parameters The operation does not have any parameters, and can be invoked with an empty POST body. ### Output The operation returns the updated resource, with all references within populated with the current display string. ### Example #### Request ```http POST /fhir/R4/Observation/[id]/$refresh-reference-display ``` --- ## Resource $resend import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; The `$resend` operation manually triggers subscription notifications for a specific resource. This is invaluable for debugging subscription workflows, recovering from failed notifications, and replaying events during development or after system issues. When a subscription notification fails or you need to reprocess a resource through your subscription pipeline, `$resend` lets you re-trigger the workflow without modifying the underlying resource—ensuring your integrations stay synchronized. ### Use Cases - **Failed Notification Recovery**: Retry subscription notifications that failed due to temporary network issues or downstream service outages - **Debugging Subscriptions**: Test subscription logic during development by manually triggering notifications - **Data Synchronization**: Force re-sync of specific resources to external systems after fixing integration issues - **Selective Replay**: Re-trigger a specific subscription for a resource instead of all subscriptions - **Integration Testing**: Verify that subscriptions fire correctly without creating new test data :::note[Admin Required] The User, Bot, or ClientApplication invoking this operation must have [project admin credentials](/docs/access/admin). ::: ### Parameters The operation takes an optional `option` parameter, which is an object containing three fields: | Option | Description | Data Type | Default Value | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ------------- | | `verbose` | Indicates if verbose logging should be enabled. | `boolean` | `false` | | `interaction` | [`Subscriptions`](/docs/api/fhir/resources/subscription) can be configured to trigger only when a resource is created or deleted as opposed to any update. This option allows you to specify which interaction type will be sent. | `update` | `create` | `delete` | `update` | | `subscription` | A specific [`Subscription`](/docs/api/fhir/resources/subscription) to trigger, formatted as `Subscription/`. If left undefined, all [`Subscriptions`](/docs/api/fhir/resources/subscription) will be triggered. | `string` | `undefined` | ### Invoke the `$resend` operation **TypeScript** {`const medplum = new MedplumClient(); // auth... await medplum.post(medplum.fhirUrl(, , '$resend'), { verbose: true, interaction: 'update', subscription: 'Subscription/123' }); `} **CLI** ```bash medplum login medplum post '//$resend' {"verbose":"true","interaction":"update","subscription":"Subscription/123"} ``` **cURL** ```bash curl 'https://api.medplum.com/fhir/R4///$resend' \ -X 'POST' \ -H 'authorization: Bearer MY_ACCESS_TOKEN' \ -H 'content-type: application/fhir+json' \ --data-raw '{"verbose":"true","interaction":"update","subscription":"Subscription/123"}' ``` #### Output If successful, you will receive the following OperationOutcome ```js { "resourceType": "OperationOutcome", "id": "ok", "issue": [ { "severity": "information", "code": "informational", "details": { "text": "All OK" } } ] } ``` ### Related - [Subscriptions Guide](/docs/subscriptions) - Complete guide to FHIR Subscriptions in Medplum - [Bot $execute](/docs/api/fhir/operations/bot-execute) - Execute bots triggered by subscriptions - [Subscription Resource](/docs/api/fhir/resources/subscription) - FHIR Subscription resource reference - [Admin Access](/docs/access/admin) - Admin credentials required for this operation - [FHIR Subscriptions](https://hl7.org/fhir/R4/subscription.html) - FHIR specification for Subscription --- ## Resource $graph The `$graph` operation fetches all resources related to a given resource as defined by a `GraphDefinition`. This allows you to retrieve a complete graph of related resources in a single request. ### Use Cases - **Patient Summary with Related Resources**: Fetch a patient along with their conditions, medications, and allergies in a single request - **Care Team Retrieval**: Load a care team with all practitioners, organizations, and the patient - **Profile Validation Setup**: Load all profiles needed to validate a resource - **UI Data Loading**: Efficiently load complex data relationships for clinical dashboards ### Invocation ``` GET [base]/[ResourceType]/[id]/$graph?graph=[GraphDefinition name] ``` ### Input Parameters | Parameter | Cardinality | Type | Description | |-----------|-------------|------|-------------| | `graph` | 1..1 | `string` | The name of the GraphDefinition to apply | ### Output Returns a `Bundle` of type `collection` containing all resources found by traversing the graph. ### GraphDefinition Structure A `GraphDefinition` defines how to traverse from a starting resource to related resources. It consists of: - **start**: The resource type where the graph begins - **link**: An array of links defining how to find related resources #### Link Types Links can be defined using two methods: ##### 1. FHIRPath Links Use a FHIRPath expression to evaluate references on the current resource: ```json { "path": "subject", "target": [ { "type": "Patient", "link": [] } ] } ``` ##### 2. Search Links Use search parameters to find resources that reference the current resource: ```json { "target": [ { "type": "Observation", "params": "subject={ref}" } ] } ``` The `{ref}` placeholder is replaced with the current resource reference. ### Example #### GraphDefinition ```json { "resourceType": "GraphDefinition", "name": "patient-with-observations", "status": "active", "start": "Patient", "link": [ { "target": [ { "type": "Observation", "params": "subject={ref}", "link": [ { "path": "performer", "target": [ { "type": "Practitioner" } ] } ] } ] }, { "path": "generalPractitioner", "target": [ { "type": "Practitioner" } ] } ] } ``` #### Request ```http GET /fhir/R4/Patient/patient123/$graph?graph=patient-with-observations ``` #### Response ```json { "resourceType": "Bundle", "type": "collection", "entry": [ { "resource": { "resourceType": "Patient", "id": "patient123", "generalPractitioner": [ { "reference": "Practitioner/dr-smith" } ] } }, { "resource": { "resourceType": "Observation", "id": "obs1", "subject": { "reference": "Patient/patient123" }, "performer": [ { "reference": "Practitioner/dr-jones" } ] } }, { "resource": { "resourceType": "Practitioner", "id": "dr-smith" } }, { "resource": { "resourceType": "Practitioner", "id": "dr-jones" } } ] } ``` ### Behavior 1. **Starting Resource**: Reads the resource specified in the URL 2. **Link Traversal**: Recursively follows all links defined in the GraphDefinition 3. **Deduplication**: Removes duplicate resources from the result 4. **Caching**: Caches resources during traversal to avoid redundant reads 5. **Canonical Resolution**: Supports resolving canonical URLs to resources #### Safety Limits - **Maximum Resources**: 1000 resources per request - **Maximum Depth**: 5 levels of nested links - **Search Cardinality**: Respects the `max` field in links (defaults to 20, max 5000) ### Link Path Expressions FHIRPath expressions can target: - **Reference fields**: `subject`, `performer`, `author` - **Canonical fields**: `instantiatesCanonical`, `profile` For canonical fields, the operation searches for resources with matching `url` values. ### Search Parameter Links Search links must include `{ref}` in the params string: ```json { "type": "Observation", "params": "subject={ref}&status=final" } ``` Multiple search parameters can be combined with `&`. ### Error Responses | Status Code | Description | |-------------|-------------| | `400 Bad Request` | Missing `graph` parameter | | `400 Bad Request` | Missing or incorrect `start` type in GraphDefinition | | `400 Bad Request` | Invalid link configuration | | `400 Bad Request` | Link target params missing `{ref}` | | `404 Not Found` | GraphDefinition not found | | `404 Not Found` | Starting resource not found | ### Example GraphDefinitions #### Patient Summary with Related Resources ```json { "name": "patient-summary", "start": "Patient", "link": [ { "target": [ { "type": "Condition", "params": "subject={ref}" }, { "type": "MedicationRequest", "params": "subject={ref}" }, { "type": "AllergyIntolerance", "params": "patient={ref}" } ] } ] } ``` #### Care Team ```json { "name": "care-team-graph", "start": "CareTeam", "link": [ { "path": "participant.member", "target": [ { "type": "Practitioner" }, { "type": "Organization" } ] }, { "path": "subject", "target": [ { "type": "Patient" } ] } ] } ``` ### Related Documentation - [FHIR $graph Operation](https://hl7.org/fhir/resource-operation-graph.html) - [GraphDefinition Resource](https://www.hl7.org/fhir/graphdefinition.html) - [FHIRPath](https://hl7.org/fhirpath/) --- ## ClientApplication $rotate-secret import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import ExampleCode from '!!raw-loader!@site/../examples/src/api/fhir/operations/rotate-client-secret.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; The `$rotate-secret` operation securely rotates client application credentials with zero downtime. It uses a two-phase rotation process: first moving the current secret to a "retiring" state while generating a new primary secret, then later removing the retiring secret—ensuring that active clients aren't immediately locked out during credential updates. Regular credential rotation is a security best practice that limits the impact of compromised credentials and meets compliance requirements for many healthcare security frameworks. ### Use Cases - **Security Best Practices**: Implement regular credential rotation schedules (e.g., every 90 days) - **Incident Response**: Quickly rotate credentials when a potential compromise is detected - **Compliance Requirements**: Meet HIPAA, SOC 2, or other security framework requirements for credential management - **Zero-Downtime Rotation**: Update credentials without disrupting active integrations during the transition period - **Automated Credential Management**: Integrate with CI/CD pipelines or secret management tools for automated rotation ### Invoke the `$rotate-secret` operation ``` [baseUrl]/ClientApplication/[id]/$rotate-secret ``` :::warning[Privileged Operation] This operation is only available to Project Admins and Super Admins. ::: ### Parameters | Name | Type | Description | Required | | ---------------- | -------- | --------------------------------------------------------------------------------------------- | -------------- | | `secret` | `string` | Rotate the primary secret, generating a new one and placing the old one into `retiringSecret` | No (see below) | | `retiringSecret` | `string` | Rotate the retiring secret, removing it from use | No | :::note[Mutually Exclusive Parameters] One and only one of the `secret` and `retiringSecret` parameters must be provided, and must match the corresponding value in the `ClientApplication`. ::: ### Output The operation returns the `ClientApplication` resource with the updated secret value. - If the `secret` is provided and matches the current `ClientApplication.secret`, that value is copied into `ClientApplication.retiringSecret` (overwriting any other value there), and a new secret is securely generated and placed into `ClientApplication.secret`; both secrets can be used to grant application access - If the `retiringSecret` parameter is provided and matches the current `ClientApplication.retiringSecret` value, the `retiringSecret` is removed from the resource and can no longer be used to grant application access ### Examples **Fully rotate client secret**: {ExampleCode} ### Related - [ClientApplication Guide](/docs/auth/client-credentials) - Understanding client credentials authentication - [Admin Access](/docs/access/admin) - Admin credentials required for this operation - [Authentication Overview](/docs/auth) - Medplum authentication methods - [Compliance](/docs/compliance) - Security and compliance for healthcare applications - [SMART App Launch](/docs/integration/smart-app-launch) - OAuth-based authentication for FHIR applications --- ## Resource $set-accounts Medplum implements a custom `$set-accounts` operation to manage account references. **This is the recommended way to manage account references for all resources**. ### Use Cases - **Multi-Tenant Data Segmentation**: Assign resources to specific accounts for organization-based data isolation - **Organization-Based Access Control**: Set account references to enable access policies based on organizational membership - **Patient Compartment Management**: Propagate account assignments to all resources in a patient's compartment for consistent access - **Batch Account Assignment**: Update account references across large sets of resources during migrations or onboarding This operation sets the target resource's `meta.accounts` references and optionally propagates changes to resources in that resources's compartment. This is useful when you need to ensure consistent `meta.accounts` access across all resources related to a patient, for example. ### Required access Calling `$set-accounts` requires explicit permission to run the operation. Generic read, create, update, or delete access to the target resource type does not automatically grant access to `$set-accounts`. If a `User`, `Bot`, or `ClientApplication` receives `403 Forbidden` when calling `$set-accounts`, confirm that the principal's access configuration grants the operation in addition to the resource interactions needed for the target resources. :::warning[] It is recommended that you run this operation with `Prefer: respond-async` header to avoid any timeouts while waiting for resources in patient compartment to update. ::: For example, when this operation is used to add a new organization to a patient's `meta.accounts`, it can update both the patient and all resources in the patient's compartment to include a new organization in their `meta.accounts` lists. Additionally, each resource's _meta.compartment_ will also be updated to include the references in _meta.accounts_. :::note[] Resources in a patient's compartment are defined by the [FHIR Patient CompartmentDefinition](https://hl7.org/fhir/R4/compartmentdefinition-patient.html). This includes resources where the patient is the `subject`, as well as resources that are directly linked to the patient through specific references (`performer`, `author`, `participant`, etc.). ::: ### Invoke the `$set-accounts` operation ``` POST [base]/R4///$set-accounts ``` #### Input The input is a [FHIR Parameters](/docs/api/fhir/resources/parameters) resource containing: - `accounts` a reference to set in each resource's _meta.accounts_ - `propagate` an optional boolean, which instructs the operation to also update resources in the target compartment Example request payload: ```http POST /fhir/R4/Patient/f1dc4eed-0b7f-4c23-9059-d4b672cb9177/$set-accounts ``` ```json { "resourceType": "Parameters", "parameter": [ { "name": "accounts", "valueReference": { "reference": "Organization/" } }, { "name": "accounts", "valueReference": { "reference": "Practitioner/" } }, { "name": "propagate", "valueBoolean": true } ] } ``` #### Output The output is a [FHIR Parameters](/docs/api/fhir/resources/parameters) resource containing: - `resourcesUpdated` The number of resources that were updated Example response if patient has 3 resources in their compartment: ```json { "resourceType": "Parameters", "parameter": [ { "name": "resourcesUpdated", "valueInteger": 3 } ] } ``` --- ## StructureDefinition $expand-profile The `$expand-profile` operation expands a `StructureDefinition` profile by recursively loading all nested `StructureDefinition` resources referenced in the profile's element type definitions. This is useful for obtaining a complete set of profiles needed to validate or render resources conforming to a profile. ### Use Cases - **Profile Validation Setup**: Load all profiles needed to validate a resource against a profile hierarchy - **UI Schema Generation**: Load all profiles to generate a complete UI schema with proper constraints - **Profile Documentation**: Generate documentation that includes all referenced profiles - **Implementation Guide Support**: Work with complex profile hierarchies from implementation guides like US Core ### Invocation ``` GET [base]/StructureDefinition/$expand-profile?url=[profile URL] ``` ### Input Parameters | Parameter | Cardinality | Type | Description | |-----------|-------------|------|-------------| | `url` | 1..1 | `string` | The canonical URL of the StructureDefinition to expand | ### Output Returns a `Bundle` of type `searchset` containing the requested profile and all nested StructureDefinition resources. ### Behavior 1. **Profile Lookup**: Searches for a StructureDefinition with the given URL 2. **Element Scanning**: Scans the profile's `snapshot.element` array for type profiles 3. **Recursive Loading**: Recursively loads all referenced profiles 4. **Deduplication**: Avoids loading the same profile multiple times 5. **Depth Limiting**: Limits recursion to 10 levels to prevent infinite loops ### Example #### Request ```http GET /fhir/R4/StructureDefinition/$expand-profile?url=http://example.org/fhir/StructureDefinition/custom-patient ``` #### Response ```json { "resourceType": "Bundle", "type": "searchset", "entry": [ { "fullUrl": "https://api.medplum.com/fhir/R4/StructureDefinition/custom-patient-id", "resource": { "resourceType": "StructureDefinition", "id": "custom-patient-id", "url": "http://example.org/fhir/StructureDefinition/custom-patient", "name": "CustomPatient", "status": "active", "kind": "resource", "type": "Patient", "snapshot": { "element": [ { "id": "Patient.identifier", "path": "Patient.identifier", "type": [ { "code": "Identifier", "profile": [ "http://example.org/fhir/StructureDefinition/custom-identifier" ] } ] } ] } } }, { "fullUrl": "https://api.medplum.com/fhir/R4/StructureDefinition/custom-identifier-id", "resource": { "resourceType": "StructureDefinition", "id": "custom-identifier-id", "url": "http://example.org/fhir/StructureDefinition/custom-identifier", "name": "CustomIdentifier", "status": "active", "kind": "complex-type", "type": "Identifier" } } ] } ``` ### Example Usage #### Profile Validation Setup Load all profiles needed to validate a resource: ```typescript const bundle = await medplum.get( 'StructureDefinition/$expand-profile?url=http://example.org/fhir/StructureDefinition/my-profile' ); // Extract all profiles for validator const profiles = bundle.entry.map(e => e.resource); ``` #### UI Schema Generation Load all profiles to generate a complete UI schema: ```typescript const bundle = await medplum.get( 'StructureDefinition/$expand-profile?url=http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient' ); // Use profiles to build form fields with proper constraints ``` #### Profile Documentation Generate documentation that includes all referenced profiles: ```typescript const bundle = await medplum.get( 'StructureDefinition/$expand-profile?url=http://example.org/fhir/StructureDefinition/custom-resource' ); // Generate documentation for each profile for (const entry of bundle.entry) { generateDocumentation(entry.resource); } ``` ### Version Resolution When multiple versions of a profile exist, the operation returns the **latest version** based on the `version` field (sorted descending). ### Error Responses | Status Code | Description | |-------------|-------------| | `400 Bad Request` | Missing `url` parameter | | `400 Bad Request` | Profile with the specified URL not found | ### Notes - The operation only includes profiles referenced in `element.type.profile` - Base FHIR types are not included in the expansion - The response includes the original requested profile as the first entry - Circular references are handled gracefully through deduplication ### Related Documentation - [StructureDefinition Resource](https://www.hl7.org/fhir/structuredefinition.html) - [Profiling FHIR](https://www.hl7.org/fhir/profiling.html) - [FHIR Implementation Guides](/docs/fhir-datastore/profiles) --- ## User $rescope The `$rescope` operation moves a [`User`](/docs/api/fhir/medplum/user) between **server scope** (not owned by any Project) and **project scope** (owned by a specific Project). This controls which Project the `User` resource itself belongs to — it is distinct from a [`ProjectMembership`](/docs/api/fhir/medplum/projectmembership), which controls access. See [Project vs Server Scoped Users](/docs/user-management/project-vs-server-scoped-users) for background on how scoping works. ``` POST [base]/User/[id]/$rescope ``` :::warning[Privileged Operation] - **Rescoping to project scope** requires **Super Admin** privileges. - **Rescoping to server scope** requires **Super Admin** or **Project Admin** privileges (project admins may only release users belonging to their own project). ::: :::caution[Irreversibility for Project Admins] A project admin who releases a user to server scope **cannot reverse the change** — only a super admin can re-assign a server-scoped user to a project. Confirm the action before proceeding. ::: ### App UI Two widgets in the Medplum App invoke this operation: - **Project Admin Config** — available to project admins and super admins at [`https://app.medplum.com/admin/config`](https://app.medplum.com/admin/config). Allows releasing a project-scoped user to server scope. Super admins can also assign server-scoped users to a project from this page. - **Super Admin Panel** — available to super admins at [`https://app.medplum.com/admin/super`](https://app.medplum.com/admin/super). Provides the same Rescope User widget with full cross-project visibility. ### Parameters The input is a [FHIR Parameters](/docs/api/fhir/resources/parameters) resource: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `scope` | `code` | Yes | Target scope: `"project"` or `"server"` | | `project` | `Reference(Project)` | Conditional | Required when `scope` is `"project"`. The Project the user will be scoped to. | ### Output Returns the updated [`User`](/docs/api/fhir/medplum/user) resource with `User.project` set (project scope) or cleared (server scope). ### Examples #### Release a user to server scope **TypeScript**: ```typescript await medplum.post(`fhir/R4/User/${userId}/$rescope`, { resourceType: 'Parameters', parameter: [ { name: 'scope', valueCode: 'server' }, ], }); ``` **cURL**: ```bash curl -X POST 'https://api.medplum.com/fhir/R4/User/example-user-id/$rescope' \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "scope", "valueCode": "server" } ] }' ``` #### Assign a user to a project (super admin only) **TypeScript**: ```typescript await medplum.post(`fhir/R4/User/${userId}/$rescope`, { resourceType: 'Parameters', parameter: [ { name: 'scope', valueCode: 'project' }, { name: 'project', valueReference: { reference: `Project/${projectId}` } }, ], }); ``` **cURL**: ```bash curl -X POST 'https://api.medplum.com/fhir/R4/User/example-user-id/$rescope' \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "scope", "valueCode": "project" }, { "name": "project", "valueReference": { "reference": "Project/example-project-id" } } ] }' ``` **Response** (200 OK): ```json { "resourceType": "User", "id": "example-user-id", "email": "alice@example.com", "firstName": "Alice", "lastName": "Smith", "project": { "reference": "Project/example-project-id", "display": "My Project" } } ``` ### Behavior #### Rescope to project - Requires the target `Project` to exist. - Rejects if the user is already scoped to the specified project. - Rejects if the user holds a [`ProjectMembership`](/docs/api/fhir/medplum/projectmembership) in any **other** project — remove those memberships before rescoping. - Sets `User.project` to the target project reference. - Runs inside a serializable transaction to prevent race conditions with concurrent membership changes. #### Rescope to server - Rejects if the user is already server-scoped. - Clears `User.project` from the user resource. - Existing `ProjectMembership` resources are left in place; clean them up separately if needed. ### Error Responses | Status | Description | |--------|-------------| | `200 OK` | Rescope successful — returns the updated `User` | | `400 Bad Request` | Invalid `scope` value; missing `project` when `scope` is `"project"`; user already in the target scope; or user holds memberships in another project | | `403 Forbidden` | Caller lacks sufficient privileges, or project admin attempted to rescope a user from a different project | | `404 Not Found` | User or target project not found | ### See Also - [Project vs Server Scoped Users](/docs/user-management/project-vs-server-scoped-users) - [User Management Guide](/docs/user-management) - [User $update-email](/docs/api/fhir/operations/user-update-email) — update a user's email address - [User Resource](/docs/api/fhir/medplum/user) - [ProjectMembership Resource](/docs/api/fhir/medplum/projectmembership) --- ## User $update-email This operation updates both the [`User`](/docs/api/fhir/medplum/user) resource (for authentication) and optionally the associated profile resource (Patient, Practitioner, or RelatedPerson) with the new email address. ### Use Cases - **Patient Email Changes**: Update a patient's email address for both login credentials and contact information - **Provider Contact Updates**: Keep practitioner email addresses synchronized between authentication and profile records - **Credential Recovery Scenarios**: Update email addresses during account recovery workflows - **User Migration**: Change email addresses as part of user account migration or consolidation ``` POST [base]/User/[id]/$update-email ``` :::warning[Privileged Operation] This operation requires **Project Admin** privileges ::: :::caution[User vs Profile Email] In Medplum, the `User.email` field controls authentication and login, while the profile resource's `telecom` field displays contact information. If you update only the profile resource email, users will not be able to log in with the new email address. Use this operation to update both atomically. ::: ### Parameters The input is a [FHIR Parameters](/docs/api/fhir/resources/parameters) resource containing: | Parameter | Type | Required | Description | | ----------------------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `email` | `string` | Yes | The new email address to set on the User | | `updateProfileTelecom` | `boolean` | No | If `true`, also updates the email in the profile resource's `telecom` field (recommended). Default: `false` | | `skipEmailVerification` | `boolean` | No | If `true`, skips email verification entirely: no `UserSecurityRequest` is created and no email is sent. Default: `false` | | `sendEmail` | `boolean` | No | If `false`, creates the `verify-email` `UserSecurityRequest` but does not send the built-in verification email. Use this to send your own branded email from a [Bot](/docs/bots) subscribed to `UserSecurityRequest`, the same pattern as `sendEmail: false` on invite and password reset. Default: `true` | ### Output The operation returns the updated [`User`](/docs/api/fhir/medplum/user) resource with: - `email` set to the new email address - `emailVerified` set to `false` (unless verification was skipped) If `updateProfileTelecom` is `true`: - Adds the new email to the profile's `telecom` array with `use: 'work'` - Marks the old email in `telecom` with `use: 'old'` ### Examples **Request**: **TypeScript**: ```typescript await medplum.post(`fhir/R4/User/${userId}/$update-email`, { resourceType: 'Parameters', parameter: [ { name: 'email', valueString: 'newemail@example.com' }, { name: 'updateProfileTelecom', valueBoolean: true }, ], }); ``` ```bash curl -X POST 'https://api.medplum.com/fhir/R4/User/example-user-id/$update-email' \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/fhir+json" \ -d '{ "resourceType": "Parameters", "parameter": [ { "name": "email", "valueString": "newemail@example.com" }, { "name": "updateProfileTelecom", "valueBoolean": true } ] }' ``` **Response** (200 OK): ```json { "resourceType": "User", "id": "example-user-id", "email": "newemail@example.com", "emailVerified": false, "firstName": "Alice", "lastName": "Smith", "project": { "reference": "Project/example-project-id" } } ``` ### Finding User ID from ProjectMembership To update a patient's email when you only have their Patient ID: ```typescript // Find the user via ProjectMembership const memberships = await medplum.searchResources('ProjectMembership', { profile: profileReference, }); const userId = memberships[0].user?.reference?.split('/')[1]; // Update the email await medplum.post(`fhir/R4/User/${userId}/$update-email`, { resourceType: 'Parameters', parameter: [ { name: 'email', valueString: 'newemail@example.com' }, { name: 'updateProfileTelecom', valueBoolean: true }, ], }); ``` ### Error Responses | Status Code | Description | | ----------------- | ------------------------------------------------------------------------- | | `200 OK` | Email successfully updated | | `400 Bad Request` | Invalid parameters or attempting to update profile for server-scoped user | | `403 Forbidden` | Insufficient permissions or user from different project | | `404 Not Found` | User not found | ### See Also - [User Management Guide](/docs/user-management) - [Project vs Server Scoped Users](/docs/user-management/project-vs-server-scoped-users) - [User Resource](/docs/api/fhir/medplum/user) --- ## Resource $validate import ExampleCode from '!!raw-loader!@site/../examples/src/api/fhir/operations/validate-a-resource.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; The `$validate` operation checks whether a FHIR resource conforms to the base specification and any applicable profiles without actually creating or updating it. This is essential for catching data quality issues early, providing real-time feedback in forms, and testing integrations before committing data to the system. **Use Cases:** - **Form validation**: Validate resources as users complete forms to provide immediate feedback on data entry errors before submission - **Integration testing**: Pre-validate incoming data from external systems to catch structural issues before attempting to persist records - **Development and debugging**: Test resource structures during development to ensure compliance with FHIR specifications and custom profiles - **Two-phase commits**: Check validity of proposed changes before executing transactions, enabling rollback-safe workflows Medplum implements the FHIR [`$validate` operation](https://www.hl7.org/fhir/resource-operation-validate.html) > This operation may be used during design and development to validate application design. It can also be used at run-time. One possible use might be that a client asks the server whether a proposed update is valid as the user is editing a dialog and displays an updated error to the user. The operation can be used as part of a light-weight two phase commit protocol but there is no expectation that the server will hold the content of the resource after this operation is used, or that the server guarantees to successfully perform an actual create, update or delete after the validation operation completes. ### Invoke the validate operation ``` [base]/[Resource]/$validate ``` For example: ```bash curl 'https://api.medplum.com/fhir/R4/Patient/$validate' \ -X POST \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -d '{"resourceType":"Patient"}' ``` The [MedplumClient](/docs/sdk/core.medplumclient) TypeScript class provides a `validateResource` convenience method: {ExampleCode} #### Valid Response Example outcome when the resource is valid: ```json { "resourceType": "OperationOutcome", "id": "ok", "issue": [ { "severity": "information", "code": "informational", "details": { "text": "All OK" } } ] } ``` #### Invalid Response Example outcome when the resource is not valid: ```json { "resourceType": "OperationOutcome", "id": "3820fda7-c6d4-4207-b597-90631f0881f9", "issue": [ { "severity": "error", "code": "structure", "details": { "text": "Invalid additional property \"Patient.foo\"" }, "expression": ["Patient.foo"] } ] } ``` ### Related - [MedplumClient.validateResource](/docs/sdk/core.validateresource) - TypeScript SDK method - [CodeSystem $validate-code](/docs/api/fhir/operations/codesystem-validate-code) - Validate coded values - [ValueSet $validate-code](/docs/api/fhir/operations/valueset-validate-code) - Validate codes against value sets - [FHIR Basics](/docs/fhir-basics) - Understanding FHIR resources and data structures - [FHIR $validate Operation](https://www.hl7.org/fhir/resource-operation-validate.html) - FHIR specification --- ## ValueSet $expand import ExampleCode from '!!raw-loader!@site/../examples/src/api/fhir/operations/valueset-expand.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; The `$expand` operation returns all codes contained in a ValueSet, making it ideal for populating dropdown menus, typeahead search fields, and code pickers in clinical applications. Instead of storing and managing large code lists client-side, you can dynamically fetch the allowed values for any coded field. This operation supports filtering and pagination, enabling responsive user interfaces that can search through thousands of codes (like SNOMED CT or LOINC) while only loading relevant results as the user types. ### Use Cases - **Dropdown Population**: Fetch all valid options for coded fields like gender, marital status, or allergy types - **Typeahead Search**: Power autocomplete fields that search through large code systems as users type - **Form Builders**: Dynamically load code options when rendering questionnaires or data entry forms - **Code Picker Components**: Build searchable code selection interfaces with pagination support - **Validation UI**: Show users the valid options when they've entered an invalid code ### Invoke the `$expand` operation ``` [base]/ValueSet/$expand ``` #### Parameters | Name | Type | Description | Required | | --------------------- | ---------- | --------------------------------------------------------------------------- | -------- | | `url` | `string` | The URL of the value set to expand | Yes | | `filter` | `string` | A filter to apply to the value set | No | | `offset` | `number` | The offset to start the expansion | No | | `count` | `number` | The number of results to return | No | | `displayLanguage` | `string` | The [language code][bcp47] to search with `filter` | No | | `excludeNotForUI` | `boolean` | Exclude abstract concepts that shouldn't be displayed in user interfaces | No | | `includeDesignations` | `boolean` | Include translations and synonyms (designations) in the expansion results | No | | `valueSet` | `ValueSet` | An inline ValueSet resource to expand (alternative to providing a URL) | No | [bcp47]: https://developer.mozilla.org/en-US/docs/Glossary/BCP_47_language_tag #### Output The result of the expansion as a `ValueSet` resource. The results of the expansion operation are in the `ValueSet.expansion` field. #### Examples Query all values for a ValueSet: ```bash curl 'https://api.medplum.com/fhir/R4/ValueSet/$expand?url=http%3A%2F%2Fhl7.org%2Ffhir%2FValueSet%2Fadministrative-gender' \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer MY_ACCESS_TOKEN" ``` ```json { "resourceType": "ValueSet", "url": "http://hl7.org/fhir/ValueSet/administrative-gender", "expansion": { "offset": 0, "contains": [ { "system": "http://hl7.org/fhir/administrative-gender", "code": "female", "display": "Female" }, { "system": "http://hl7.org/fhir/administrative-gender", "code": "male", "display": "Male" }, { "system": "http://hl7.org/fhir/administrative-gender", "code": "other", "display": "Other" }, { "system": "http://hl7.org/fhir/administrative-gender", "code": "unknown", "display": "Unknown" } ] } } ``` Filter by search string: ```bash curl 'https://api.medplum.com/fhir/R4/ValueSet/$expand?url=http%3A%2F%2Fhl7.org%2Ffhir%2FValueSet%2Fadministrative-gender&filter=f' \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer MY_ACCESS_TOKEN" ``` ```json { "resourceType": "ValueSet", "url": "http://hl7.org/fhir/ValueSet/administrative-gender", "expansion": { "offset": 0, "contains": [ { "system": "http://hl7.org/fhir/administrative-gender", "code": "female", "display": "Female" } ] } } ``` The [MedplumClient](/docs/sdk/core.medplumclient) TypeScript class provides a `valueSetExpand` convenience method: {ExampleCode} ### Related - [ValueSet $validate-code](/docs/api/fhir/operations/valueset-validate-code) - Validate codes against expanded value sets - [CodeSystem $lookup](/docs/api/fhir/operations/codesystem-lookup) - Look up details about individual codes - [Medplum Terminology Guide](/docs/terminology) - Overview of terminology services - [React Components](/docs/react) - Medplum React components including code inputs - [FHIR ValueSet $expand](https://hl7.org/fhir/R4/valueset-operation-expand.html) - FHIR specification for $expand --- ## ValueSet $validate-code import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import ExampleCode from '!!raw-loader!@site/../examples/src/api/fhir/operations/operation-framework.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; The `$validate-code` operation checks whether a code is a valid member of a specific ValueSet. Unlike CodeSystem validation (which checks if a code exists anywhere in a code system), ValueSet validation confirms that a code is within the specific subset of codes allowed for a particular use-such as the list of valid allergy severity codes or permitted procedure types. This is critical for enforcing business rules and data quality, ensuring that clinical data conforms to the specific terminology constraints defined for your application's forms and interfaces. ### Use Cases - **Input Validation**: Ensure user-selected codes or codes from external systems are valid for specific fields or expected ValueSets before processing - **Constraint Enforcement**: Validate that incoming data meets ValueSet-based constraints defined in FHIR profiles or implementation guides like US Core - **Custom Picklist Validation**: Verify selections against organization-specific ValueSets that subset larger code systems ### Invoke the `$validate-code` operation ``` [baseUrl]/ValueSet/$validate-code [baseUrl]/ValueSet/[id]/$validate-code ``` ### Parameters | Name | Type | Description | Required | | ----------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | | `url` | `uri` | The canonical URL of the ValueSet to validate against | No\* | | `code` | `string` | The code to look up. | No | | `system` | `string` | The canonical URL of the code system the code belongs to. | No | | `coding` | `Coding` | Look up via full Coding. | No | | `codeableConcept` | `CodeableConcept` | Look up multiple related codes. | No | | `display` | `string` | Additionally validate the given display text | No | | `abstract` | `boolean` | Whether codes labeled as abstract should be included. Abstract codes represent broad categories or groupings rather than specific concepts, e.g. "Vital Signs" | No | * If no `url` is provided, the operation must be invoked on a specific `ValueSet` instance. :::note[Required Parameters] Although no parameters are strictly required by the operation, at least one of the following must be provided: - Both `code` and `system` parameters (and optionally `display`) - The `coding` parameter - The `codeableConcept` parameter with at least one contained `coding` ::: ### Output The operation returns a `Parameters` resource containing the validation result. | Name | Type | Description | Required | | --------- | --------- | ------------------------------------------------------------- | -------- | | `result` | `boolean` | Whether or not any given coding is included in the `ValueSet` | Yes | | `display` | `string` | The display text of the included code | No | #### Error Responses **Example**: Specified `ValueSet` could not be found by URL (400 Bad Request) ```js { "resourceType": "OperationOutcome", "issue": [ { "severity": "error" "code": "invalid", "details": { "text": "ValueSet http://example.com/ValueSet/missing not found" }, } ] } ``` ### Examples **Request**: **TypeScript** {ExampleCode} **cURL** ```bash curl 'https://api.medplum.com/fhir/R4/ValueSet/$validate-code' \ -X POST \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer $MY_ACCESS_TOKEN" \ -d '{"resourceType":"Parameters","parameter":[{"name":"url","valueUri":"http://hl7.org/fhir/ValueSet/condition-severity"},{"name":"coding","valueCoding":{"system":"http://snomed.info/sct","code":"255604002"}}]}' ``` **Response**: (200 OK) ```js { "resourceType": "Parameters", "parameter": [ { "name": "result", "valueBoolean": true }, { "name": "display", "valueString": "Mild (qualifier value)" } ] } ``` ### Related - [ValueSet $expand](/docs/api/fhir/operations/valueset-expand) - Expand a value set to see all valid codes - [CodeSystem $validate-code](/docs/api/fhir/operations/codesystem-validate-code) - Validate codes against code systems - [CodeSystem $lookup](/docs/api/fhir/operations/codesystem-lookup) - Look up details about codes - [Medplum Terminology Guide](/docs/terminology) - Overview of terminology services - [FHIR ValueSet $validate-code](https://hl7.org/fhir/R4/valueset-operation-validate-code.html) - FHIR specification --- ## Overview Medplum is based on the [FHIR API](https://www.hl7.org/fhir/R4/). Medplum is "FHIR Native," using version 4.0.1 (aka R4), meaning the service stores data in the actual FHIR format, documented [here](/docs/api/fhir/resources). Medplum also implements [custom resource types](/docs/api/fhir/medplum) to handle user management, authentication, and authorization. The base URL for hosted Medplum is https://api.medplum.com/fhir/R4 --- ## Account import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/account.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A financial tool for tracking value accrued for a particular purpose. In the healthcare field, used to track charges for a patient, cost centers, etc. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The Account resource acts as a central record against which charges, payments, and adjustments are applied. It contains information about which parties are responsible for payment of the account. While the Account does conceptually have a balance, expressing that balance directly as a resource property is challenging due to the complexity of pricing contracts. An operation to retrieve the current balance of an account is in consideration as future work. **Relationships** The Account itself does not include information about the charges, payments or adjustments, but rather those resources, such as ChargeItem point to the account to which they apply. Payment and adjustment resources have not yet been developed. **Background and Context** The Account resource can be considered a "bucket" to which ChargeItem resources are linked. These charges are processed by a billing system, which determines the responsible parties for the balance of the account. The billing system then submits claims or sends statements to the appropriate parties. Once payment is received, an adjustment is applied to the Account. The internal calculation of balances and allocation of responsibility is expected to be internal to the billing systems. Only the inputs and outputs of the billing process is communicated in the relevant financial FHIR resources. **Referenced By** ChargeItem Encounter EpisodeOfCare Invoice --- ## ActivityDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/activitydefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; This resource allows for the definition of some activity to be performed, independent of a particular patient, practitioner, or other performance context. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is a definition resource from a FHIR workflow perspective - see Workflow, specifically Definition. An ActivityDefinition is a shareable, consumable description of some activity to be performed. It may be used to specify actions to be taken as part of a workflow, order set, or protocol, or it may be used independently as part of a catalog of activities such as orderables. For more information on how activity definitions can be used to construct request resources, see the Applying an ActivityDefinition topic below. **Relationships** In general, an activity definition is simply a conceptual description of some specific action that should be taken. An instance of an ActivityDefinition does not indicate that any action has been performed (as an event resource does), nor does it indicate the actual intent to carry out any particular action (as a request resource does). Instead, an activity definition provides a reusable template that can be used to construct specific request resources such as ServiceRequest and MedicationRequest. Note that this is conceptually similar to the Task resource as well, with the distinction being that ActivityDefinition represents the description of a task in the abstract, while the Task resource is used to track a specific instance of a task as it moves through the steps of a workflow. **Referenced By** CarePlan CatalogEntry Communication DeviceRequest EvidenceVariable FamilyMemberHistory MessageDefinition NutritionOrder PlanDefinition Procedure ServiceRequest Task --- ## AdverseEvent import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/adverseevent.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Actual or potential/avoided event causing unintended physical injury resulting from or contributed to by medical care, a research study or other healthcare setting factors that requires additional monitoring, treatment, or hospitalization, or that results in death. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** AdverseEvent is an event resource from a FHIR workflow perspective - see Workflow Event This resource applies to events that occur during the course of medical care or medical research which may impact an individual as the recipient of care or the participant in a research study. There are also events that occur within a care setting that might or might not impact an individual but had the potential to cause an adverse event. Health care organizations monitor and report both adverse events as well as events that had the potential to cause patient harm. Data are often aggregated for reporting purposes. An adverse event is the result of an intervention that caused unintentional harm to a specific subject or group of subjects. Examples of adverse events include the administration of an incorrect drug or an incorrect dose of a drug causing an adverse reaction, the use of an implanted device that causes an infection, or a biologic used during a research study that causes unanticipated renal failure. These events are characterized by the need to capture cause and effect (although they might not be known at the time of the event), severity, and outcome. The context of an adverse event is also important. A subject may have condition(s) or current treatments (medications, diet, devices) that impact their response to a newly introduced medication, device or procedure. Knowledge of these variables is essential in establishing a cause and effect relationship for an adverse event. A potential adverse event may also be called a near miss or an error. These are also events but because they were detected did not cause harm to a subject. Examples of potential adverse events include a product problem such as a faulty pacemaker that is detected prior implantation, a doctor working simultaneously on two electronic health records realizing the order for a drug was entered on the incorrect patient and then canceling the order, or a patient with a peanut allergy notices that his hospital dinner tray includes peanuts, and he does not eat the peanuts. **Relationships** The AdverseEvent resource is designed to represent events that have a harmful impact on a subject, or had the potential to cause harm to a subject but were avoided. In the course of medical care there are many actions that may impact how a subject responds to a particular treatment impacting patient safety. Therefore the AdverseEvent resource may reference multiple other resources to represent the context or details of an adverse event including but not limited to Observation, Condition, MedicationAdminsitration, Immunization, Procedure, or ResearchStudy. A DetectedIssue reference is also related to the context of an AdverseEvent to the extent that a known risk for a potential issue such as a drug-drug interaction is documented. If in the context of a known issue, and adverse event occurs, citing this relationship is important for preventing such an occurrence in the future. The AdverseEvent resource should not be used when a more specific resource exists. The AllergyIntolerance resource is a case specific means of capturing the condition of an allergy or intolerance and the criticality (or potential for future harm) based on the response of a particular individual. The Clinical Reasoning module provides resources and operations to enable the representation, distribution, and evaluation of clinical knowledge artifacts such as clinical decision support rules, quality measures, order sets, and protocols. The suite of resources within the clinical reasoning module should be used to capture clinical quality measures and clinical protocols that help drive clinical best practices. The AdverseEvent resource is not intended to be used to capture potential subject risk in a prospective manner. A more appropriate resource for this purpose would be Risk Assessment which captures predicted outcomes for a patient or population on the basis of source information. Examples include a prognosis statement for a particular condition, risk of health outcome (heart attack, particular type of cancer) on the basis of lifestyle factors and/or family history or list of potential health risks based on a patient's genetic analysis. --- ## AllergyIntolerance import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/allergyintolerance.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Risk of harmful or undesirable, physiological response which is unique to an individual and associated with exposure to a substance. Refer to the [US Core AllergyIntolerance profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-allergyintolerance.html) details on representing specific substances, statuses and reaction types. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A record of a clinical assessment of an allergy or intolerance; a propensity, or a potential risk to an individual, to have an adverse reaction on future exposure to the specified substance, or class of substance. Where a propensity is identified, to record information or evidence about a reaction event that is characterized by any harmful or undesirable physiological response that is specific to the individual and triggered by exposure of an individual to the identified substance or class of substance. Substances include, but are not limited to: a therapeutic substance administered correctly at an appropriate dosage for the individual; food; material derived from plants or animals; or venom from insect stings. Note for Reviewers Presently open issues for this resource: This resource represents a condition of susceptibility to a substance, with a list of supporting events and/or symptoms, and has no direct relationship to an event reporting framework; this will be re-assessed when adverse event reporting resource(s) and/or profiles are added to FHIR Other HL7 models and the openEHR archetype have "exposure date" but this is not found in any surveyed systems, so this is left as an extension (more appropriate for adverse event reporting) **Relationships** This resource is used to provide a single place within the health record to document a range of clinical statements about adverse reactions to substances/products, including: record a clinical assessment of the individual's propensity to a potential future reaction upon re-exposure record cumulative information about the reaction to each exposure, including 'no reaction' if appropriate Use to record information about the positive presence of the risk of an adverse reaction: to support direct clinical care of an individual as part of a managed adverse reaction or allergy/intolerance list to support exchange of information about the propensity and events related to adverse reactions to inform adverse reaction reporting to assist computerized knowledge-based activities such as clinical decision support and alerts Use to record information about adverse reactions to a broad range of substances, including: biological & blood products; incipients and excipients in medicinal preparations; foods; metal salts; and organic chemical compounds. Adverse reactions may be: an allergy (typically type I hypersensitivity, plus other "allergy-like" reactions, including pseudoallergy) an intolerance (typically non-immune adverse reactions that are not determined or perceived to be allergic or "allergy-like", and are to some degree idiosyncratic and/or individually specific [i.e. are not a reaction that is expected to occur with most or all patients given similar circumstances]) In clinical practice distinguishing between allergy and intolerance is difficult and might not be practical. Often the term "allergy" is used rather generically and may overlap with "intolerance", and the boundaries between these concepts might not be well-defined or understood. As noted above, the term "intolerance" should generally be applied to a propensity for adverse reactions which is either determined (to the extent that is possible) or perceived to not be allergic or "allergy-like". If it is not possible to determine whether a particular propensity condition is an allergy or an intolerance, then the type element should be omitted from the resource. Identification of the type of reaction is not a proxy for seriousness or risk of harm to the patient, which is better expressed in the documentation of the clinical manifestation and the assessment of criticality. The sensitivity in the case of either an allergy or intolerance is unique to the individual, and is distinguished from those reactions that are a property of the circumstance, such as toxicity of a food or drug, overdose, drug-drug, drug-food, or drug-disease interaction (which are reactions that would be expected to occur for any individual given the same circumstances). The risk of an adverse reaction event or manifestation should not be recorded without identifying a proposed causative substance (including pharmaceutical products) or class of substance. If there is uncertainty that a specific substance is the cause, this uncertainty can be recorded using the 'verificationStatus' data element. If there are multiple possible substances that may have caused a reaction/manifestation, each substance should be recorded using a separate instance of this resource with the 'verificationStatus' set to an initial state of 'unconfirmed' so that adverse reaction checking can be supported in clinical systems. If a substance, agent or class is later proven not to be the cause for a given reaction then the 'verificationStatus' can be modified to 'refuted'. This resource has been designed to allow recording of information about a specific substance (e.g., amoxicillin, oysters, or bee sting venom) or pharmaceutical product or, alternatively, a class of substance (e.g., penicillins). If a class of substance is recorded, then identification of the exact substance can be recorded on a per exposure basis. The scope of this FHIR resource has deliberately focused on identifying a pragmatic data set that is used in most clinical systems or will be suitable for most common clinical scenarios; extensions can be used to add additional detail if required. Examples of clinical situations where the extension may be required include: a detailed allergist/immunologist assessment, for reporting to regulatory bodies or use in a clinical trial. The act of recording any adverse reaction in a health record involves the clinical assessment that a potential hazard exists for an individual if they are exposed to the same substance/product/class in the future - that is, a relative contraindication - and, in the absence of additional information indicating a higher level of potential risk, the default 'criticality' value should be set to 'Low Risk'. If a clinician considers that it is not safe for the individual to be deliberately re-exposed to the substance/product again, for example, following a manifestation of a life-threatening anaphylaxis, then the 'criticality' data element should be amended to 'High Risk'. A formal adverse event report to regulatory bodies is a document that will contain a broad range of information in addition to the specific details about the adverse reaction. The report could utilize parts of this resource plus include additional data as required per jurisdiction. An adverse reaction or allergy/intolerance list is a record of all identified propensities for an adverse reaction for the individual upon future exposure to the substance/product or class, plus provides potential access to the evidence provided by details about each reaction event, such as manifestation. Valuable first-level information that could be presented to the clinician when they need to assess propensity for future reactions are: statements about previous clinical manifestations following exposure source of the information/reporter the 'criticality' flag Second-level information can be drawn from each exposure event and links to additional detailed information such as history, examination and diagnoses stored elsewhere in the record, if it is available. AllergyIntolerance and RiskAssessment AllergyIntolerance describes a specific type of risk - propensity to reaction to a substance/product while RiskAssessment describes general risks to a subject, not generally based on a reaction. AllergyIntolerance and Immunization.reaction Immunization.reaction may be an indication of an allergy or intolerance. If this is deemed to be the case, a separate AllergyIntolerance record should be created to indicate it, as most systems will not query against past immunization.reactions. Misuse The allergy/intolerance list exists as a patient safety tool to inform decision support around ordering of medications and nutrition and to guide clinical treatment. Other reactions triggered by physical stimuli -- light, heat, cold, pressure, vibration, which may mimic allergic or intolerance reactions, should be recorded as Condition on the problem list, not using AllergyIntolerance. Not to be used to record adverse events, including failures of clinical process, interventions or products. For example, abnormal use or mistakes/errors made in maladministration of an agent or substance, incorrect dosage, mislabeling, harm or injury caused by an intervention or procedure, overdose/poisoning, etc. Not to be used as a proxy for an adverse event report. See above for how it may be used as one component of an adverse event report. Not to be used for recording alerts. Alerts are handled using Flag or - where event-specific, DetectedIssue. Not to be used for recording failed therapy.
Trial-Use Note: Requests have been received (GF#10369) to add codes to the extension-allergyintolerance-certainty value set (reaction-event-certainty), which is a required binding. The requested codes include "unknown", "ruled out" and "possible". The Patient Care WG has voted to add "unknown" to the value set, but recommends that if other terms (including "ruled out" and "possible") are desired for use in a specific setting, an extension or profile should be used. During the STU period feedback is solicited regarding (1) the need and desirability of adding the code "unknown" to the value set, vs. omitting the element if the certainty is not known; and (2) whether or not additional codes besides "unknown" should be added to the value set for the core specification, or whether additional codes, if needed, should be added in an extension/profile. Feedback is welcome here .
**Referenced By** AdverseEvent ClinicalImpression FamilyMemberHistory NutritionOrder --- ## Appointment import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/appointment.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A booking of a healthcare event among patient(s), practitioner(s), related person(s) and/or device(s) for a specific date/time. This may result in one or more Encounter(s). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Appointment resources are used to provide information about a planned meeting that may be in the future or past. The resource only describes a single meeting, a series of repeating visits would require multiple appointment resources to be created for each instance. Examples include a scheduled surgery, a follow-up for a clinical visit, a scheduled conference call between clinicians to discuss a case, the reservation of a piece of diagnostic equipment for a particular use, etc. The visit scheduled by an appointment may be in person or remote (by phone, video conference, etc.) All that matters is that the time and usage of one or more individuals, locations and/or pieces of equipment is being fully or partially reserved for a designated period of time. This definition takes the concepts of appointments in a clinical setting and also extends them to be relevant in the community healthcare space, and to ease exposure to other appointment / calendar standards widely used outside of healthcare. The basic workflow to create an appointment Discovery/Addressing Before an appointment can be made, the address/endpoint details of the resource that we want to schedule an appointment with must be determined. This is often based on the healthcare service type and any formatting information which indicates how to make the request. This is typically handled via the Schedule resource. Checking Availability on the Schedule (optional) This optional step permits the checking of any existing available times (Slot resources associated with a selected Schedule) that can be booked against. Just because a time is indicated as available doesn't guarantee that an appointment can be made. The booking system that is going to process the request may make other qualifying decisions to determine if the appointment can be made, such as permissions, assessments, availability of other resources, etc. This step is optional, as the creation of the appointment is never a guaranteed action. But by performing this availability check, you can increase the chances of making a successful booking. Making the Appointment Request When an appointment is required, a requester creates new Appointment resource with the Appointment.status="proposed". All included participants (optional or mandatory) should have the status="needs-action" to allow filtering and displaying appointments to user-participants for accepting or rejecting new and updated requests. Based on internal system business rules, certain statuses may be automatically updated, for example: "reject because the requested participant is on vacation" or "this type of user is not allowed to request those specific appointments". Replying to the request The reply process is simply performed by the person/system handing the requests, updating the participant statuses on the appointment as needed. If there are multiple systems involved, then these will create AppointmentResponse entries with the desired statuses. Once all participants have their participation status created/updated (and the main system marking the appointment participant records with the AppointmentResponse statuses) then the overall status of the Appointment is updated. Checking the overall status (Requester) The requester (organizer) of the appointment checks for the overall status of the appointment (and appointment responses, where applicable) using FHIR pub-sub techniques. Where the participant statuses indicate that a re-scheduling is required, then the process may start again, with other systems replying to a new set of times. Waitlisting the Appointment (optional) This optional step permits creating a waitlisted appointment. This could occur if an appointment needs to be booked into a time that is not ideal for the patient due to lack of available time slots. In this workflow, there would be two appointments, the booked appointment in the time slot that is currently available, and the waitlisted appointment with a requestedPeriod spanning the time that the patient would prefer if new slots become available. If new time slots become available during the requestedPeriod, the scheduling system, or staff at the scheduling organization, can notify the patient that a new time slot is available. If the patient chooses, the waitlisted appointment would then be booked into that specific slot, and the previously booked appointment would be canceled. The specific business process for notifying patients of new availability is not specified, and is up to the implementing system to determine. There are 2 typical workflows that occur with appointments Outlook Style - Community These types of requests are typically handled by selecting a specific time from a list of available slots, then making the request for that timeslot. Hospital Scheduling - Clinical Clinical scheduling is often far more complex in its requirements and processing. Often this involves checking multiple availabilities across multiple systems and timing with other internal systems, not just those exposed by the Slot resources. Consideration should be given to situations where scheduling needs to be handled in more of a queue-like process.
Implementation Note: Note: This type of clinical appointment scheduling has not been specifically covered with this definition of the Appointment resource (and other related resources), however if you would like to contribute to the modification of this resource to cover these use cases, please contact the HL7 Patient Administration work-group.
**Relationships** Appointment Request/Response Pattern When using a request-response style of appointment this is done using Appointment and AppointmentResponse resources. The request is made in the form of an Appointment with a proposed or pending status, and the list of actors with a participation status of "needs-action". Participants in the appointment respond with their acceptance (or not) to the appointment by creating AppointmentResponse resources. Once all the participants have replied, then the Appointment resource is able to be updated with an overall status which collates the results of all the participants and presents the approved details of the appointment. The participant type property can be used to represent a specific role that a practitioner is required to perform for the appointment. This could be specified without an actor when the actual practitioner is not known, and will be filled in closer to the scheduled time. This property must be the same between the Appointment-participant and the AppointmentResponse so that the appropriate values can be allocated. If you need multiple actors of a specific type, then multiple participants with that type value are included on the appointment. Appointment Statuses and Encounters Appointments can be considered as Administrative only, and the Encounter is expected to have clinical implications. In general, it is expected that appointments will result in the creation of an Encounter. The encounter is typically created when the service starts, not when the patient arrives. When the patient arrives, an appointment can be marked with a status of Arrived. In an Emergency Room context, the appointment Resource is probably not appropriate to be used. In these cases, an Encounter should be created. The Appointment request pattern used is different from the order-response pattern used elsewhere in FHIR. This is due to the close relationship to the iCal standard. Many non-clinical systems use generic non-health appointment systems which implement this standard, and the desire to integrate with the consumer who has no access to health based software is highly desirable. The mappings to the iCal standard have been provided to guide implementation of gateways between FHIR servers and iCal systems. Appointment Locations and Participation The location of the appointment is to be defined by using a participant that references a Location or HealthcareService resource. This permits the location to also have its availability checked via a schedule and any conflicts more easily managed. **Referenced By** AppointmentResponse CarePlan Encounter ImagingStudy --- ## AppointmentResponse import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/appointmentresponse.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A reply to an appointment request for a patient and/or practitioner(s), such as a confirmation or rejection. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Appointment resources are used to provide information about a planned meeting that may be in the future or past. They may be for a single meeting or for a series of repeating visits. Examples include a scheduled surgery, a follow-up for a clinical visit, a scheduled conference call between clinicians to discuss a case, the reservation of a piece of diagnostic equipment for a particular use, etc. The visit scheduled by an appointment may be in person or remote (by phone, video conference, etc.) All that matters is that the time and usage of one or more individuals, locations and/or pieces of equipment is being fully or partially reserved for a designated period of time. This definition takes the concepts of appointments in a clinical setting and also extends them to be relevant in the community healthcare space, and also ease exposure to other appointment / calendar standards widely used outside of Healthcare. The basic workflow to create an appointment Making the Appointment Request When an appointment is required, a requester creates new Appointment resource with the Appointment.status="proposed". All included participants (optional or mandatory) should have the status="needs-action" to allow filtering and displaying appointments to user-participants for accepting or rejecting new and updated requests. Based on internal system business rules, certain statuses may be automatically updated, for example: "reject because the requested participant is on vacation" or "this type of user is not allowed to request those specific appointments". Replying to the request The reply process is simply performed by the person/system handing the requests updating the participant statuses as needed. If there are multiple systems involved, then these will create AppointmentResponse entries with the desired statuses. Once all participants have their participation status created/updated (and the main system marking the appointment participant records with the AppointmentResponse statuses) then the overall status of the Appointment is updated. **Referenced By** ImagingStudy --- ## AuditEvent import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/auditevent.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A record of an event made for purposes of maintaining a security log. Typical uses include detection of intrusion attempts and monitoring for inappropriate usage. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The audit event is based on the IHE-ATNA Audit record definitions, originally from RFC 3881 , and now managed by DICOM (see DICOM Part 15 Annex A5 ). ASTM E2147 – Setup the concept of security audit logs for healthcare including accounting of disclosures IETF RFC 3881 – Defined the Information Model (IETF rule forced this to be informative) DICOM Audit Log Message – Made the information model Normative, defined Vocabulary, Transport Binding, and Schema IHE ATNA – Defines the grouping with secure transport and access controls; and defined specific audit log records for specific IHE transactions. NIST SP800-92 – Shows how to do audit log management and reporting – consistent with our model HL7 PASS – Defined an Audit Service with responsibilities and a query interface for reporting use ISO 27789 – Defined the subset of audit events that an EHR would need ISO/HL7 10781 EHR System Functional Model Release 2 ISO 21089 Trusted End-to-End Information Flows This resource is managed collaboratively between HL7, DICOM, and IHE. The primary purpose of this resource is the maintenance of security audit log information. However, it can also be used for any audit logging needs and simple event-based notification. **Background and Context** All actors - such as applications, processes, and services - involved in an auditable event should record an AuditEvent. This will likely result in multiple AuditEvent entries that show whether privacy and security safeguards, such as access control, are properly functioning across an enterprise's system-of-systems. Thus, it is typical to get an auditable event recorded by both the application in a workflow process and the servers that support them. For this reason, duplicate entries are expected, which is helpful because it may aid in the detection of. For example, fewer than expected actors being recorded in a multi-actor process or attributes related to those records being in conflict, which is an indication of a security problem. There may be non-participating actors, such as trusted intermediary, that also detect a security relevant event and thus would record an AuditEvent, such as a trusted intermediary. Security relevant events are not limited to communications or RESTful events. They include: software start-up and shutdown; user login and logout; access control decisions; configuration events; software installation; policy rules changes; and manipulation of data that exposes the data to users. See the Audit Event Sub-Type vocabulary for guidance on some security relevant events. The content of an AuditEvent is intended for use by security system administrators, security and privacy information managers, and records management personnel. This content is not intended to be accessible or used directly by other healthcare users, such as providers or patients, although reports generated from the raw data would be useful. An example is a patient-centric accounting of disclosures or an access report. Servers that provide support for AuditEvent resources would not generally accept update or delete operations on the resources, as this would compromise the integrity of the audit record. Access to the AuditEvent would typically be limited to security, privacy, or other system administration purposes. Relationship of AuditEvent and Provenance resources are often (though not exclusively) created by the application responding to the create/read/query/update/delete/execute etc. event. A Provenance resource contains overlapping information, but is a record-keeping assertion that gathers information about the context in which the information in a resource "came to be" in its current state, e.g., whether it was created de novo or obtained from another entity in whole, part, or by transformation. Provenance resources are prepared by the application that initiates the create/update of the resource and may be persisted with the AuditEvent target resource. --- ## Basic import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/basic.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Basic is used for handling concepts not yet defined in FHIR, narrative-only resources that don't map to an existing resource, and custom resources not appropriate for inclusion in the FHIR specification. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Basic is a special type of resource. Unlike all other resources, it doesn't correspond to a specific pre-defined HL7 concept. Instead, it's a placeholder for any resource-like concept that isn't already defined in the HL7 specification. The Basic resource is intended for use in three circumstances: When an implementer needs a resource concept that is likely to be defined by HL7 in the future but they have not yet done so (due to bandwidth issues, lack of sufficient requirements, lower prioritization, etc.) When there's a need to convey a narrative-only construct that doesn't neatly correspond to one of the other resources, either because it combines aspects of several resources (e.g. Assessment + Plan) or because the allowed content is flexible such that the system can't be totally sure what sort of content might have been included in the narrative text. Other than the circumstances above, this resource will see minimal use. To keep the FHIR specification manageable, it cannot incorporate every site-specific requirement that might be needed in some implementation somewhere. This set of resources likely won't ever be officially defined in HL7. There's also a fourth circumstance: An implementer wishes to convey information that could/should be conveyed using a standard resource, however they want to represent the information in a custom format that isn't aligned with the official resource's elements. While this resource would be the preferred way of meeting that use-case because it will at least be wire-format compatible, such a use would not be conformant because making use of the Basic resource would prevent the healthcare-related information from being safely processed, queried and analyzed by other conformant systems. Implementers don't need to be concerned with which of the three categories their desired resource fits within. If they need a resource and it clearly doesn't fit one of the ones currently defined, they should use Basic. **Background and Context** Basic defines only a minimal set of data elements - those necessary to identify what kind of resource it represents and those necessary to support resource compartmenting. All other data elements are represented using the extension mechanism. It's entirely possible to have a Basic resource instance with nothing other than narrative, a subject and code. And, in practice, that's all many systems will understand. --- ## Binary import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/binary.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A resource that represents the data of a single raw artifact as digital content accessible in its native format. A Binary resource can contain any content, whether text, image, pdf, zip archive, etc. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** There are situations where it is useful or required to handle pure binary content using the same framework as other resources. Typically, this is when the binary content is referred to from other FHIR Resources. Using the same framework means that the existing servers, security arrangements, code libraries, etc. can handle additional related content. Typically, Binary resources are used for handling content such as: CDA Documents (i.e. with XDS) PDF Documents Images A binary resource can contain any content, whether text, image, pdf, zip archive, etc. These resources are served in their native form on the rest interface, but can also be represented in XML, JSON, or other formats, such as when including these resources in a Bundle (used when it is convenient to include these in the response directly rather than leaving them by reference). **Relationships** When a FHIR server finds it convenient to manage the content within the same overall REST framework as the other resources, the Binary resource is generally used as the target in the Attachment data type to reference content via the .url element, such as in the DocumentReference and Media resources. Consequently, the Binary resource can be a target wherever the Attachment data type is used such as in the DocumentReference resource. The DocumentReference and Media resources allow conveying binary content (via attachment) or pointing to one (as a Binary or non-FHIR URI) along with the metadata around that resource, and as such are searchable. Binary resources do not support 'search'. While CDA and PDF documents are conveyed as Binary (because they cannot be expressed natively in FHIR), FHIR Documents do not need to be similarly encoded and can be sent natively in FHIR using Bundle. However, in some situations FHIR Documents may be sent as a Binary if there is a need to treat them the same as other types of documents or binary files. The Binary resource does not convey context of the file. If the context (information such as author, procedure, technique, etc.) should be conveyed, Media or DocumentReference resources are appropriate. The Binary resource may be used to convey actual binary file content conveyed by those resources. The Media resource is preferred for handling images, but this is not possible when the content is already binary (e.g. in some uses of IHE XDS ). **Referenced By** CatalogEntry ImplementationGuide --- ## BiologicallyDerivedProduct import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/biologicallyderivedproduct.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A material substance originating from a biological entity intended to be transplanted or infused into another (possibly the same) biological entity. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage**
Trial-Use Note: Note that this content is preliminary has not undergone proper review by the appropriate Workgroups.
A material substance originating from a biological entity intended to be transplanted or infused into another (possibly the same) biological entity. Examples include: hematopoietic stem cells (bone marrow, peripheral blood, or cord blood extraction) blood (whole, extracted cells, plasma, etc.) organs tissues (porcine valves, skin, bovine cardiac tissue, etc.) manipulated cells (e.g. CAR T-cells) The workflow using this resource (e.g., request, administration) should be discussed and implemented in a consistent way as other similar resources are handled (e.g., device, medication) **Relationships** This resource relates to these other resource: ProcedureRequest (for collection) Patient ("receiver" and "source") Practitioner (who collected product) Substance (product processing) DiagnosticReport (containing HLA-typing) BiologicallyDerivedProduct ("parent" product for multi-day collections) Procedure (one for collection and one for transplantation, will need to add BiologicallyDerivedProduct to the "usedReference") --- ## BodyStructure import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/bodystructure.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Record details about an anatomical structure. This resource may be used when a coded concept does not provide the necessary detail needed for the use case. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The BodyStructure resource contains details about the anatomical location of a specimen or body part, including patient information, identifiers, as well as text descriptions and images. It provides for the addition of qualifiers such as laterality and directionality to the anatomic location for those use cases where precoordination of codes is not possible. The BodyStructure resource supports recording and tracking of an anatomic location or structure on a patient outside the context of another resource. For example it can be the target of a Procedure resource or Observation resource. **Relationships** The BodyStructure resource is not intended to substitute for precoordination of codes. If precoordination of codes is supported by an implementation the codeableConcept should be used. This resource is not intended for describing the type of anatomical location but rather a specific body site on a specific patient. --- ## Bundle import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/bundle.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A container for a collection of resources. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** One common operation performed with resources is to gather a collection of resources into a single instance with containing context. In FHIR this is referred to as "bundling" the resources together. These resource bundles are useful for a variety of different reasons, including: Returning a set of resources that meet some criteria as part of a server operation (see RESTful Search) Returning a set of versions of resources as part of the history operation on a server (see History) Sending a set of resources as part of a message exchange (see Messaging) Grouping a self-contained set of resources to act as an exchangeable and persistable collection with clinical integrity - e.g. a clinical document (see Documents) Creating/updating/deleting a set of resources on a server as a single operation (including doing so as a single atomic transaction) (see Transactions) Storing a collection of resources **Relationships** There are two ways to collect resources together for transport and persistence purposes - contained resources, and bundles. There is an important difference between the two: Contained resources are "in" the container resource - they can only ever be interpreted and/or changed in the context of the container A bundle is a collection of resources that can have an independent existence - for example, they might also be accessed directly using the RESTful API In addition to these two technical mechanisms, there are three administrative and infrastructure resources which also support grouping of content. These resources do not contain resources directly, but instead use [Reference] to point to the grouped resources: The List resource – Enumerates a flat collection of resources and provides features for managing the collection. While a particular List instance may represent a "snapshot", from a business process perspective the notion of "List" is dynamic – items are added and removed over time. The list resource references other resources. Lists may be curated and have specific business meaning. The Group resource – Defines a group of specific people, animals, devices, etc. by enumerating them, or by describing qualities that group members have. The group resource refers to other resources, possibly implicitly. Groups are intended to be acted upon or observed as a whole; e.g. performing therapy on a group, calculating risk for a group, etc. This resource will commonly be used for public health (e.g. describing an at-risk population), clinical trials (e.g. defining a test subject pool) and similar purposes. The Composition resource – Defines a set of healthcare-related information that is assembled together into a single logical document that provides a single coherent statement of meaning, establishes its own context and that has clinical attestation with regard to who is making the statement. The composition resource provides the basic structure of a FHIR document. The full content of the document is expressed using a Bundle. Compositions will often reference Lists as the focus of particular sections. These three resources represent meaningful groupings of the resources they refer to (e.g. a discharge medication List, a Group of participants for a clinical trial, a set of resources that form a signed document), while a Bundle is merely is a container for resources used for transfer and storage. This list is not exhaustive; other resources also provde grouping functionality. --- ## CapabilityStatement import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/capabilitystatement.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A Capability Statement documents a set of capabilities (behaviors) of a FHIR Server for a particular version of FHIR that may be used as a statement of actual server functionality or a statement of required or desired server implementation. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The capability statement is a key part of the overall conformance framework in FHIR. It is used as a statement of the features of actual software, or of a set of rules for an application to provide. This statement connects to all the detailed statements of functionality, such as StructureDefinitions and ValueSets. This composite statement of application capability may be used for system compatibility testing, code generation, or as the basis for a conformance assessment. For further information about Conformance testing, see Conformance Rules and Profiling FHIR. Specifically, capability statements are used in one of three ways: Instance | implementation must be present and software may be present Capability | implementation must be absent, software must be present Requirements | implementation and software must be absent Instance: Describe an actual implementation In this scenario, the capability statement describes the capabilities of a deployed and configured solution available at a particular access point or set of access points. The statement describes exactly how to interface with that deployed solution and thus provides for a degree of self-configuration of software solutions. This is the type of statement that FHIR restful solutions are expected to make available on invocation of the capabilities operation. It is also the type of statement that forms a basis for the testing, certification or commissioning of specific software installations. Capability: Describe software solution capabilities In this scenario, the capability statement describes generic capabilities of a software application or component solution. The solution might be available for purchase or other acquisition and might be deployed and configured at any number of independent sites. Because it is not dependent on any particular implementation, the profile cannot provide specific details such as endpoint addresses. It may also need to document various configurations in which the application can be set up or describe the degree of customizability associated with the solution. This type of statement may be used as a marketing tool by software and system developers to formally describe their capabilities. It can also be used as the basis for conformance testing of software solutions independent of a particular installation. Requirements: Describe a desired solution In this scenario, the capability statement describes the capabilities of a desired system. It might be used as part of an architectural design process to document needed system capabilities, or might be used as part of an RFP process to formally document the requirements of a requested solution and to document the criteria by which proposals will be evaluated. These three types of profiles can be used together. A requirements statement can be compared against the solution statements proffered by respondents to an RFP. A solution statement for a software package forms the starting point for the implementation statement associated with a particular installation of that software package. CapabilityStatements of type "requirement" describe what capabilities are potentially relevant; additional documentation or extensions (see capabilitystatement-expectation) within the CapabilityStatement are expected to make more explicit statements of degree of expectation associated with each capability. **Background and Context** Capability Statements provide for a degree of automatic configuration and adaptation. However, capturing absolutely every variation that could impact the interoperability of two systems, let alone keeping that detailed information up-to-date as systems evolve through maintenance and upgrades, is rarely practical. Therefore, capability statements should be seen as an interim step. They provide a degree of automation. However, they also provide a great deal of human-readable content that can minimize the need for direct communication between the operators of the systems being configured to interoperate. Supporting Multiple Versions Applications may implement multiple versions. If they do, then a CapabilityStatement describes the system's support for a particular version of FHIR, and the system will have multiple statements, one for each version it supports. For further information, see Managing Multiple Versions, and the $versions operation. Mixed Normative Content While the core of the CapabilityStatement resource is Normative, many of the flags that indicate exactly how the system operates are marked as trial-use. Roughly, the portions of the resource that correspond to OpenAPI document elements are normative. Applications looking for normative stability should only use the normative parts of the resource, and not populate or ignore the portions labelled trial-use. To assist with this, clients can ask for the server to return a 'Normative content only' CapabilityStatement using the mode parameter on /metadata. Community discussion regarding more capable, efficient and computable representations of an applications capabilities may lead to change to the trial-use parts of this resource or the creation of new resources and/or functionality in future versions of this specification. **Referenced By** TestScript --- ## CarePlan import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/careplan.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Describes the intention of how one or more practitioners intend to deliver care for a particular patient, group or community for a period of time, possibly limited to care for a specific condition or set of conditions. Refer to the [US Core CarePlan profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-careplan.html). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** CarePlan is one of the request resources in the FHIR workflow specification. Care Plans are used in many areas of healthcare with a variety of scopes. They can be as simple as a general practitioner keeping track of when their patient is next due for a tetanus immunization through to a detailed plan for an oncology patient covering diet, chemotherapy, radiation, lab work and counseling with detailed timing relationships, pre-conditions and goals. They may be used in veterinary care or clinical research to describe the care of a herd or other collection of animals. In public health, they may describe education or immunization campaigns. This resource takes an intermediate approach to complexity. It captures basic details about who is involved and what actions are intended without dealing in discrete data about dependencies and timing relationships. These can be supported where necessary using the extension mechanism. The scope of care plans may vary widely. Examples include: Multi-disciplinary cross-organizational care plans; e.g. An oncology plan including the oncologist, home nursing staff, pharmacy and others Plans to manage specific disease/condition(s) (e.g. nutritional plan for a patient post bowel resection, neurological plan post head injury, pre-natal plan, post-partum plan, grief management plan, etc.) Decision support generated plans following specific practice guidelines (e.g. stroke care plan, diabetes plan, falls prevention, etc.) Self-maintained patient or care-giver authored plans identifying their goals and an integrated understanding of actions to be taken This resource can be used to represent both proposed plans (for example, recommendations from a decision support engine or returned as part of a consult report) as well as active plans. The nature of the plan is communicated by the status. Some systems may need to filter CarePlans to ensure that only appropriate plans are exposed via a given user interface. **Relationships** For simplicity's sake, CarePlan allows the inline definition of activities as part of a plan using the activity.detail element. However, activities can also be defined using references to the various "request" resources. These references could be to resources with a status of "planned" or to an active order. It is possible for planned activities to exist (e.g. appointments) without needing a CarePlan at all. CarePlans are used when there's a need to group activities, goals and/or participants together to provide some degree of context. CarePlans can be tied to specific Conditions, however they can also be condition-independent and instead focused on a particular type of care (e.g. psychological, nutritional) or the care delivered by a particular practitioner or group of practitioners. An ImmunizationRecommendation can be interpreted as a narrow type of CarePlan dealing only with immunization events. Where such information could appear in either resource, the immunization-specific resource is preferred. CarePlans represent a specific plan instance for a particular patient or group. It is not intended to be used to define generic plans or protocols that are independent of a specific individual or group. CarePlan represents a specific intent, not a general definition. Protocols and order sets are supported through PlanDefinition. **Referenced By** DiagnosticReport GuidanceResponse ImagingStudy Media MedicationRequest MedicationStatement Observation Procedure QuestionnaireResponse ServiceRequest --- ## CareTeam import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/careteam.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The Care Team includes all the people and organizations who plan to participate in the coordination and delivery of care for a patient. Refer to the [US Core CarePlan profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-careplan.html). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The CareTeam includes all the people, teams, and organizations who plan to participate in the coordination and delivery of care for a single patient or a group (such as a married couple in therapy or a support group). CareTeam can also be organizationally assigned without a subject in context, such as a code blue team or emergency response team. This is not limited to practitioners, but may include other caregivers such as family members, guardians, the patient themself, or others. The Care Team, depending on where used, may include care team members specific to a particular care plan, an episode, an encounter, or may reflect all known team members across these perspectives. An individual's CareTeam can be dynamic over time, such that there can be transience of team members, such as a rehabilitation team. **Relationships** Care Team is distinct from Group. Group is patient-independent and identifies an undifferentiated set of individuals who are intended to be the target of one or more clinical activities (e.g. set of clinical trial participants, set of individuals impacted by or at risk of a public health event, a herd or flock, etc.) The CareTeam resource establishes a set of relationships and roles and is specific to a particular Patient. The actors are the individual members or organized group of individuals. CareTeam can be referenced by EpisodeOfCare, Encounter, or CarePlan to identify the set of individuals (and their respective roles) who are intended to be involved in providing the care defined by those resources. **Referenced By** CarePlan ChargeItem Communication CommunicationRequest Consent Contract DeviceRequest DiagnosticReport EpisodeOfCare ImagingStudy Media MedicationRequest Observation ServiceRequest Task --- ## CatalogEntry import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/catalogentry.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Catalog entries are wrappers that contextualize items included in a catalog. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The CatalogEntry resource (formally known as EntryDefinition) allows additional information specification for items that may be found in the following types of clinical collections, including but not limited to: A pharmacy formulary A laboratory compendium A radiology compendium of exams that may be ordered A dietary nutrition order catalog A supply charge master A CDS catalog of knowledge artifacts The Catalog profile of the Composition resource is used to organize catalog entries. Catalogs are composed of nestable sections which contain catalog entries. Catalog entries wrap the items that are orderable or selectable in a catalog and any other required supporting entries. Examples of information provided by CatalogEntry includes whether the status of the item in the catalog is "active", "inactive", "legacy", or "informative" or whether the item is an orderable item in the context of this catalog (or catalog section). the relation between the item and other items ("contains", "includes", "requires"). Catalog entries also support relationships to other catalog entries such as whether a product is now replaced by another product in a product catalog. Please refer to the Boundaries and Relationships part of this document for more information on the topic. **Relationships** This CatalogEntry resource wraps and contextualizes definitional items that make up a catalog such as the following resources: Medication Device Activity Definition (including laboratory service definitions) Specimen Definition ... It is important to note that CatalogEntry shall not specify attributes that are pertinent to the item that it wraps if those attributes hold true regardless of the catalog context. For instance, a catalog entry should generally not provide attributes to describe the hierarchical relationship of an item to other items (e.g., drug class or other such ontological relationships) since that information holds outside of the context of a catalog and thus should reside in the referenced item itself. A catalog entry may, however, specify a status of a given item in the context of the catalog such as whether that item is orderable a given institution or context. Catalog entries can also be related to one another. Here again, it is important to note that only relationships that hold within the context of a catalog and not generally for the item referenced by the catalog entry should be considered. For instance, in the context of a given laboratory service compendium, a laboratory order may be preferred over other laboratory orders. The other laboratory orders in this use case may thus be related to the preferred test using an 'is-replaced-by' relationship and assigned a new status of 'retired'. Other definitional relationships such as the specimen or expected input and output observation definitions associated with a particular laboratory test, relationships that generally hold outside of a laboratory catalog context, should reside at the level of the definitional resource itself. That is, laboratory tests often state the required input observation specifications, the format of the expected observation output, and the attributes of the required specimen and such relationships are probably best described outside of the CatalogEntry within the referenced item. --- ## ChargeItem import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/chargeitem.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The resource ChargeItem describes the provision of healthcare provider products for a certain patient, therefore referring not only to the product, but containing in addition details of the provision, like date, time, amounts and participating organizations and persons. Main Usage of the ChargeItem is to enable the billing process and internal cost allocation. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Tracking Financial information is vital in Patient Administration and Finance systems in most Healthcare Organizations. The resource ChargeItem describes the charge for provision of healthcare provider products for a certain patient, therefore referring not only to the product, but containing in addition details of the provision, like date, time, amounts and participating organizations and persons. Main Usage of the ChargeItem is to enable the billing process and internal cost allocation. They are created as soon as the products are planned or provisioned, references to Encounters and/or Accounts can be maintained in a later process step. The target of ChargeItem.definition may provide information on the Charge code such as pricing and inclusion/exclusion rules as well as factors that apply under certain conditions. In many cases however this information may have been drawn from sources outside of FHIR depending on the distribution format of the code catalogue. The ChargeItem assumes that such information is either implicitly known by the communicating systems or explicitly shared through the ChargeItem.definition. Therefore explicit pricing information is not shared within the ChargeItem resource. Also, the systems posting the ChargeItems are not expected to apply the rules associated with the charge codes as they may not know the whole context of the patient/encounter to evaluate such rules. It lies within the responsibility of a billing engine, to collect the ChargeItems in the context of an Account or Encounter at a certain point in time (e.g. discharge of the patient) and to evaluate the associated rules resulting in some of the ChargeItems to be set to the status "not billable" in case the rules exclude them from being billed, or to create financial transactions according to base price and factors. Additional references to Encounter/EpisodeOfCare, Patient/Group and Services provide further context to help billing systems determine the appropriate account and establish the clinical/financial context to evaluate the rules associated with the charge codes. **Relationships** This resource is not an actual financial transaction (such as an item on an invoice or any concise monetary amount being transferred from one Account to another) but is the base administrative data that may be used by a billing engine to create the financial transactions based on rules, factors and base prices associated with the charge code. Unlike the Financial Transaction the ChargeItem primarily describes the provision, whereas the Financial Transaction documents cash flow. Therefore, the Financial Transaction results from ChargeItems created via the subsequent billing- or cost allocation process. The actual financial transaction resulting from the evaluation of these rules against the clinical and financial context may be represented in formats appropriate to the financial realm. These are considered out of scope for the FHIR Standard, as they are not specific to the healthcare domain. The FHIR Claim resource does contain line items, and this ChargeItem resource provides the source material for the billing engine to create the items on the claim (which may be different due to business rules). **Referenced By** Invoice --- ## ChargeItemDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/chargeitemdefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The ChargeItemDefinition resource provides the properties that apply to the (billing) codes necessary to calculate costs and prices. The properties may differ largely depending on type and realm, therefore this resource gives only a rough structure and requires profiling for each type of billing code system. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The ChargeItemDefinition holds all relevant billing information pertaining to a billing code or a product. Many billing systems have rules associated with the code which may define whether a billing code is applicable in a specific context or whether certain billing details (surcharges, discounts) apply. ChargeItemDefinitions may also be used to hold pricing information pertaining to only one specific instance of a product (e.g. Device, Medication or Substance) which may be a requirement for custom made devices or medications or other products where prices vary by instance. Rules for applicability may include a billing code is only applicable in an outpatient setting a surcharge that applies only to services provided outside regular business hours a discount that applies only to patients up to the age of 14 a billing code that may only be charged once per Encounter a billing code that can only be charged in combination with another code from the same billing system A billing engine will apply the ChargeItemDefinitions to all ChargeItems within a specific Account in order to select all billable items an acquire the pricing details to create an Invoice. However, ChargeItems may be validated against a ChargeItemDefinition at any point in time, using the $apply-Operation **Relationships** While the ChargeItem resource represents the occurrence of a billing code (for services rendered or products supplied to a specific patient), the ChargeItemDefinition represents the properties and rules that a billing engine needs to determine the pricing and applicability of a billing code in the context of a specific setting. Billing Codes may be defined in a CodeSystem Resource, however, each of these codes - if it is used in a financial context - can have a large number of business rules and properties associated with it, that lay outside the scope of CodeSystem and may change in time independently from the underlying Code's validity period. Even though CodeSystem can associate a concept with any number of additional properties, such a construct would be very hard to maintain. This is why the ChargItemDefinition has been introduced. It allows to maintain the attributes of each billing code independently and has a wider scope to also include business rules. In most use cases, billing codes will be both defined as semantical concepts in a CodeSystem and as a financial artifact in ChargeItemDefinition. ChargeItemDefinitions that pertain to codes from the same CodeSystem may be aggregated using the Catalog profile for Composition. Resources representing products that can be sold or billed (Device, Medication, Substance...), do not specify financial details for the product instance. Most of the time, such products are not billed seperately but are a part of a generic billing code that covers both the product and the service rendered. In some occasions however, product prices may vary depending on type or manufactuer or even on instance (e.g. custom made products). For these cases, ChargeItemDefinitions may be created to specify the financial details for a product kind, type or instance. **Referenced By** ChargeItem --- ## Claim import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/claim.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A provider issued list of professional services and products which have been provided, or are to be provided, to a patient which is sent to an insurer for reimbursement. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The Claim is used by providers and payors, insurers, to exchange the financial information, and supporting clinical information, regarding the provision of health care services with payors and for reporting to regulatory bodies and firms which provide data analytics. The primary uses of this resource is to support eClaims, the exchange of information relating to the proposed or actual provision of healthcare-related goods and services for patients to their benefit payors, insurers and national health programs, for treatment payment planning and reimbursement. The Claim resource is a "request" resource from a FHIR workflow perspective - see Workflow Request. The Claim resource may be interpreted differently depending on its intended use (and the Claim.use element contains the code to indicate): claim - where the provision of goods and services is complete and adjudication under a plan and payment is sought. preauthorization - where the provision of goods and services is proposed and authorization and/or the reservation of funds is desired. predetermination - where the provision of goods and services is explored to determine what services may be covered and to what amount. Essentially a 'what if' claim. The Claim also supports: Up to a 3-tier hierarchy of Goods, products, and Services, to support simple to complex billing. Multiple insurance programs arranged in a Coordination of Benefit sequence to enable exchange with primary, secondary, tertiary etc. insurance coverages. Assignment of benefit - the benefit may be requested to be directed to the subscriber, the provider or another party. Mapping to other Claim specifications: Mappings are currently maintained by the Financial Management Work Group to UB04 and CMS1500 and are available at https://confluence.hl7.org/display/FM/FHIR+Resource+Development . Mappings to other specifications may be made available where IP restrictions permit. Additional Information Additional information regarding electronic claims content and usage may be found at: Financial Resource Status Lifecycle: how .status is used in the financial resources. Secondary Use of Resources: how resources such as Claim and ExplanationOfBenefit may used for reporting and data exchange for analytics, not just for eClaims exchange between providers and payors. Subrogation: how eClaims may handle patient insurance coverages when another insurer rather than the provider will settle the claim and potentially recover costs against specified coverages. Coordination of Benefit: how eClaims may handle multiple patient insurance coverages. Batches: how eClaims may handle batches of eligibility, claims and responses. Attachments and Supporting Information: how eClaims may handle the provision of supporting information, whether provided by content or reference, within the eClaim resource when submitted to the payor or later in a resource which refers to the subject eClaim resource. This also includes how payors may request additional supporting information from providers. **Relationships** The Claim resource is used to request the adjudication and/or authorization of a set of healthcare-related goods and services for a patient against the patient's insurance coverages, or to request what the adjudication would be for a supplied set of goods or services should they be actually supplied to the patient. When requesting whether the patient's coverage is inforce, whether it is valid at this or a specified date, or requesting the benefit details or preauthorization requirements associated with a coverage, then CoverageEligibilityRequest should be used instead. When using the resources for reporting and transferring claims data, which may have originated in some standard other than FHIR, the Claim resource is useful if only the request side of the information exchange is of interest. If, however, both the request and the adjudication information is to be reported then the ExplanationOfBenefit should be used instead. For reporting out to patients or transferring data to patient centered applications, such as patient health Record (PHR) application, the ExplanationOfBenefit should be used instead of the Claim and ClaimResponse resources as those resources may contain provider and payer specific information which is not appropriate for sharing with the patient. The eClaim domain includes a number of related resources Claim | A suite of goods and services and insurances coverages under which adjudication or authorization is requested. CoverageEligibilityRequest | A request to a payor to: ascertain whether a coverage is in-force at the current or at a specified time; list the table of benefits; determine whether coverage is provided for specified categories or specific services; and whether preauthorization is required, and if so what supporting information would be required. ClaimResponse | A payor's adjudication and/or authorization response to the suite of services provided in a Claim. Typically the ClaimResponse references the Claim but does not duplicate the clinical or financial information provided in the claim. ExplanationOfBenefit | This resource combines the information from the Claim and the ClaimResponse, stripping out any provider or payor proprietary information, into a unified information model suitable for use for: patient reporting; transferring information to a Patient Health Record system; and, supporting complete claim and adjudication information exchange with regulatory and analytics organizations and other parts of the provider's organization. **Referenced By** ClaimResponse DeviceUseStatement ExplanationOfBenefit --- ## ClaimResponse import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/claimresponse.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; This resource provides the adjudication details from the processing of a Claim resource. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The ClaimResponse resource provides application level adjudication results, or an application level error, which are the result of processing a submitted Claim resource where that Claim may be the functional corollary of a Claim, Predetermination or a Preauthorization.This resource is the only appropriate response to a Claim which a processing system recognizes as a Claim resource. This is the adjudicated response to a Claim, Predetermination or Preauthorization. The strength of the payment aspect of the response is matching to the strength of the original request. For a Claim the adjudication indicates payment which is intended to be made. For Preauthorization no payment will actually be made however funds may be reserved to settle a claim submitted later. For Predetermination no payment will actually be made and no assurance is given that the adjudication of a claim submitted later will match the adjudication provided, for example funds may have been exhausted in the interim. Only an actual claim may be expected to result in actual payment. The ClaimResponse resource may also be returned with the response for the submission of: Re-adjudication and Reversals. The ClaimResponse resource is an "event" resource from a FHIR workflow perspective - see Workflow Event. Additional Information Additional information regarding electronic claims content and usage may be found at: Financial Resource Status Lifecycle: how .status is used in the financial resources. Secondary Use of Resources: how resources such as Claim, ClaimResponse and ExplanationOfBenefit may be used for reporting and data exchange for analytics, not just for eClaims exchange between providers and payors. Subrogation: how eClaims may handle patient insurance coverages when another insurer rather than the provider will settle the claim and potentially recover costs against specified coverages. Coordination of Benefit: how eClaims may handle multiple patient insurance coverages. RealTime Exchange and Obtaining Deferred Responsess: ClaimResponses may be obtained using Polling or FHIR REST (SEARCH). Attachments and Supporting Information: how eClaims may handle the provision of supporting information, whether provided by content or reference, within the eClaim resource when submitted to the payor or later in a resource which refers to the subject eClaim resource. This also includes how payors may request additional supporting information from providers. **Relationships** The ClaimResponse resource is used to provide the results of the adjudication and/or authorization of a set of healthcare-related products and services for a patient against the patient's insurance coverages, or to respond with what the adjudication would be for a supplied set of products or services should they be actually supplied to the patient. The ExplanationOfBenefit resource is for reporting out to patients or transferring data to patient centered applications, such as patient health Record (PHR) application, the ExplanationOfBenefit should be used instead of the Claim and ClaimResponse resources as those resources may contain provider and payer specific information which is not appropriate for sharing with the patient. When using the resources for reporting and transferring claims data, which may have originated in some standard other than FHIR, the Claim resource is useful if only the request side of the information exchange is of interest. If, however, both the request and the adjudication information is to be reported then the ExplanationOfBenefit should be used instead. When responding whether the patient's coverage is inforce, whether it is valid at this or a specified date, or returning the benefit details or preauthorization requirements associated with a coverage CoverageEligibilityResponse should be used instead and be the response to a CoverageEligibilityRequest. The eClaim domain includes a number of related resources ClaimResponse | A payor's adjudication and/or authorization response to the suite of services provided in a Claim. Typically the ClaimResponse references the Claim but does not duplicate the clinical or financial information provided in the claim. ExplanationOfBenefit | This resource combines the information from the Claim and the ClaimResponse, stripping out any provider or payor proprietary information, into a unified information model suitable for use for: patient reporting; transferring information to a Patient Health Record system; and, supporting complete claim and adjudication information exchange with regulatory and analytics organizations and other parts of the provider's organization. Claim | A suite of goods and services and insurances coverages under which adjudication or authorization is requested. CoverageEligibilityResponse | The response to a request to a payor, a CoverageEligibilityRequest, to: ascertain whether a coverage is in-force at the current or at a specified time; list the table of benefits; determine whether coverage is provided for specified categories or specific services; and whether preauthorization is required, and if so what supporting information would be required. **Referenced By** Claim DeviceRequest ExplanationOfBenefit MedicationRequest ServiceRequest Task --- ## ClinicalImpression import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/clinicalimpression.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A record of a clinical assessment performed to determine what problem(s) may affect the patient and before planning the treatments or management strategies that are best to manage a patient's condition. Assessments are often 1:1 with a clinical consultation / encounter, but this varies greatly depending on the clinical workflow. This resource is called "ClinicalImpression" rather than "ClinicalAssessment" to avoid confusion with the recording of assessment tools such as Apgar score. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Performing a clinical assessment is a fundamental part of a clinician's workflow, performed repeatedly throughout the day. In spite of this - or perhaps, because of it - there is wide variance in how clinical impressions are recorded. Some clinical assessments simply result in an impression recorded as a single text note in the patient 'record' (e.g. "Progress satisfactory, continue with treatment"), while others are associated with careful, detailed record keeping of the evidence gathered and the reasoning leading to a differential diagnosis, and there is a continuum between these. This resource is intended to be used to cover all these use cases. The assessment is intimately linked to the process of care. It may occur in the context of a care plan, and it very often results in a new (or revised) care plan. Normally, clinical assessments are part of an ongoing process of care, and the patient will be re-assessed repeatedly. For this reason, the clinical impression can explicitly reference both care plans (preceding and resulting) and reference a previous impression that this impression follows. An impression is a clinical summation of information and/or an opinion formed, which is the outcome of the clinical assessment process. The ClinicalImpression may lead to a statement of a Condition about a patient. In FHIR, an assessment is typically an instrument or tool used to collect information about a patient.
Trial-Use Note: Unlike many other resources, there is little prior art with regard to exchanging records of clinical assessments. For this reason, this resource should be regarded as particularly prone to ongoing revision. In terms of scope and usage, the Patient Care workgroup wishes to draw the attention of reviewers and implementers to the following issues: When is an existing clinical impression revised, rather than a new one created (that references the existing one)? How does that affect the status? what's the interplay between the status of the diagnosis and the status of the impression? (e.g. for a 'provisional' impression, which bit is provisional?) This structure doesn't differentiate between a working and a final diagnosis. Given an answer to the previous question, should it? Further clarify around the relationship between care plan and impression is needed. Both answers to the previous questions and ongoing discussions around revisions to the care plan will influence the design of clinical impression Should prognosis be represented, and if so, how much structure should it have? Should an impression reference other impressions that are related? (how related?) Investigations - the specification needs a good value set for the code for the group, and will be considering the name "investigations" further Feedback is welcome here .
**Relationships** ClinicalImpression is the equivalent of the "A" in Weed SOAP. It is the outcome of the clinical assessment process. The ClinicalImpression may lead to a statement of a Condition about a patient. There is another related clinical concept often called an "assessment": assessment Tools such as Apgar (also known as "Assessment Scales"). This is not what the ClinicalImpression resource is about; assessment tools such as Apgar are represented as Observations, and Questionnaires may be used to help generate these. Clinical Impressions may refer to these assessment tools as one of the investigations that was performed during the assessment process. **Background and Context** An important background to understanding this resource is the FHIR Confluence page for clinical assessment . In particular, the storyboards there drove the design of the resource, and will be the basis for all examples created. PLANNED CHANGE: ClinicalImpression is one of the Event resources in the FHIR Workflow specification. As such, it is expected to be adjusted to align with the Event workflow pattern which will involve adding a number of additional data elements and potentially renaming a few elements. Any concerns about performing such alignment are welcome as ballot comments and/or tracker items. **Referenced By** Condition --- ## CodeSystem import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/codesystem.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The CodeSystem resource is used to declare the existence of and describe a code system or code system supplement and its key properties, and optionally define a part or all of its content. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The FHIR terminology specification is based on two key concepts, originally defined in HL7 v3 Core Principles : code system - declares the existence of and describes a code system or code system supplement and its key properties, and optionally defines a part or all of its content. Also known as Ontology, Terminology, or Enumeration value set - specifies a set of codes drawn from one or more code systems, intended for use in a particular context. Value sets link between CodeSystem definitions and their use in coded elements Code systems define which codes (symbols and/or expressions) exist, and how they are understood. Value sets select a set of codes from one or more code systems to specify which codes can be used in a particular context. The CodeSystem resource is used to declare the existence of a code system, and its key properties: Identifying URL and version Description, Copyright, publication date, and other metadata Some key properties of the code system itself - e.g. whether it exhibits concept permanence, whether it defines a compositional grammar, and whether the codes that it defines are case sensitive What filters can be used in value sets that use the code system in a ValueSet.compose element What concept properties are defined by the code system In addition, the CodeSystem resource may list some or all of the concepts in the code system, along with their basic properties (code, display, definition), designations, and additional properties. Code System resources may also be used to define supplements, which extend an existing code system with additional designations and properties. The CodeSystem resource is not intended to support the process of maintaining code systems. Instead, the focus is on publishing the properties and optionally the content of a code system for use throughout the FHIR eco-system, such as to support value set expansion and validation. Note that the important existing (large) code systems (SNOMED CT, LOINC, RxNorm, ICD family, etc.) all have their own maintenance systems and distribution formats, and CodeSystem is generally not an efficient way to distribute their content, though it is used as one way of declaring the filters and properties associated with those code systems. **Relationships** Code systems are used in ValueSet resources The Coding data type refers to CodeSystem resources by their canonical URL The CodeSystem resource design is based, in part, on the code system functionality described in the HL7 CTS 2 specification. A CTS2 server can be used to maintain code systems which are then published using the CodeSystem resource The CodeSystem resource documents the inherent structure and capabilities of code system, whereas the TerminologyCapabilities resource documents what a server hosting the code system is capable of The CodeSystem resource declares the existence of a code system and its key properties including its preferred identifier. The NamingSystem resource identifies the existence of a code or identifier system, and its possible and preferred identifiers. The key difference between the resources is who creates and manages them - CodeSystem resources are managed by the owner or publisher of the code system, who can properly define the code system features and content. NamingSystem resources, on the other hand, are frequently defined by 3rd parties that encounter the code system in use, and need to describe the use, but do not have the authority to define the features and content. Additionally, there may be multiple authoritative NamingSystem resources for a code system, but ideally there would be only one authoritative CodeSystem resource (identified by its canonical URL) that is provided by the code system publisher, with multiple copies distributed on additional FHIR servers or elsewhere and used where needed. Both Code System supplements and Concept Maps may be used to define relationships between concepts in different systems. ConceptMaps are assertions of the relationships between different concepts that are associated with particular contexts of use, while CodeSystem supplements are used to define inherent properties and semantics of the concepts in the code system **Background and Context** When using code systems and value sets, proper differentiation between a code system and a value set is important. This is one very common area where significant clinical safety risks occur in practice. Implementers should be familiar with the content in Using Codes in Resources. CodeSystem Identification A code system has three identifiers. The first two can be used to reference the code system in the FHIR context: CodeSystem.id: The logical id on the system that holds the CodeSystem resource instance - this typically is expected to change as the resource moves from server to server. The location URI is constructed by appending the logical id to the server base address where the instance is found and the resource type. This URI should be a resolvable URL by which the resource instance may be retrieved, usually from a FHIR server, and it may be a relative reference typically to the server base URL. CodeSystem.url: The canonical URL that never changes for this code system - it is the same in every copy. The element is named url rather than uri for legacy reasons and to strongly encourage providing a resolvable URL as the identifier whenever possible. This canonical URL is used to refer to all instances of this particular code system across all servers and systems. Ideally, this URI should be a URL which resolves to the location of the master version of the code system, though this is not always possible.
For example, the code systems published as part of the FHIR specification all have a location ("literal") URI which is the URL where they may be accessed in the FHIR specification itself. However, while a new version of the FHIR specification is being prepared, code systems that are published in the drafts will not be found in the current FHIR specification version.
Because it is common practice to copy (cache) code systems locally, most references to code systems can use either the canonical URL or the location ("literal") URL. The third code system identifier is used typically for external references to the code system outside of FHIR: CodeSystem.identifier: A system/value pair that is used to identify the code system in other contexts (such as an OID in an HL7 v3 specification) For further information regarding resource identification, see Resource Identity. This means that each code system has 2 different URIs that can be used to reference it - its canonical URL (the url element), and its local location from which it may be retrieved (which includes the id element). Because it is common practice to copy (cache) code systems locally, most references to code systems use the canonical URL. Alternatively, the identifier and version elements may be used to reference this code system in a design, a profile, a CDA template or HL7 v3 message (in the CD data type codeSystem and codeSystemVersion properties). These different contexts may make additional restrictions on the possible values of these elements. The identifier is generally not needed when using code systems in a FHIR context, where the canonical URL is always the focus. **Referenced By** Coding ConceptMap TerminologyCapabilities ValueSet --- ## Communication import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/communication.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; An occurrence of information being transmitted; e.g. an alert that was sent to a responsible provider, a public health agency that was notified about a reportable condition. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Communication is one of the event resources in the FHIR workflow specification. This resource is a record of a communication even if it is planned or has failed. A communication is a conveyance of information from one entity, a sender, to another entity, a receiver. The sender and receivers may be patients, practitioners, related persons, organizations, or devices. Communication use cases include: A reminder or alert delivered to a responsible provider A recorded notification from the nurse to the on-call physician (or any other specified person) that a patient's temperature exceeds a value A notification to a public health agency of a patient presenting with a communicable disease reportable to the public health agency Patient educational material sent by a provider to a patient Unable to deliver lab results to ordering physician Non-patient specific communication use cases may include: A nurse call from a hall bathroom Advisory for battery service from a pump **Relationships** This resource is a record of a communication that has occurred. It does not represent the actual flow of communication. While AuditEvent can track electronic disclosures of information, it cannot track conversations, phone calls, letters and other interactions that are not system-to-system. And even for system-to-system communications, the specific end recipients might not be known. Furthermore, AuditEvents are not considered to be "part" of the patient record, while Communication instances are. The Communication resource is not used as a general audit mechanism to track every disclosure of every record. Rather, it is used when a clinician or other user wants to ensure a record of a particular communication is itself maintained as part of the reviewable health record. Flag resources represent a continuous ongoing "communication" alerting anyone dealing with the patient of certain precautions to take or issues to be aware of. The flags are continuously present as an ongoing reminder. This is distinct from Communication where there is a specific intended sender and receiver and the information is delivered only once. Communication and Encounter The Communication is about the transfer of information (which might or might not occur as part of an encounter), while Encounter is about the coming together (in person or virtually) of a Patient with a Practitioner. Communication does not deal with the duration of a call, it represents the fact that information was transferred at a particular point in time. The phone calls involving the Patient should be handled using Encounter. Phone calls not involving the patient (e.g. between practitioners or practitioner to relative) that are tracked for billing or other purposes can use Communication to represent the information transferred but are not ideal to represent the call itself. A better mechanism for handling such calls will be explored in a future release. --- ## CommunicationRequest import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/communicationrequest.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A request to convey information; e.g. the CDS system proposes that an alert be sent to a responsible provider, the CDS system proposes that the public health agency be notified about a reportable condition. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** CommunicationRequest is one of the request resources in the FHIR workflow specification. This resource is a record of a request for a communication to be performed. A communication is a conveyance of information from one entity, a sender, to another entity, a receiver. The requester requests the sender to send the payload to the recipient. The sender and receivers may be patients, practitioners, related persons, organizations, and devices. Uses of communication request include: A computer-based decision-support system requesting a reminder or alert be delivered to a responsible provider A physician requesting notification from the nurse if a patient's temperature exceeds a value A monitoring system or a provider requesting a staff member or department to notify a public health agency of a patient presenting with a communicable disease reportable to the public health agency A computer-based decision-support system proposes to send educational material to a patient **Relationships** This resource is a record of a request. It does not represent the actual flow of communication. The use of CommunicationRequest excludes requests for referrals and requests for therapy or counseling which would be handled by the ServiceRequest resource. The performance of a CommunicationRequest may result in a Communication resource. **Referenced By** CarePlan ClaimResponse --- ## CompartmentDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/compartmentdefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A compartment definition that defines how resources are accessed on a server. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Each resource may belong to one or more logical compartments. A compartment is a logical grouping of resources which share a common property. Compartments have two principal roles: Function as an access mechanism for finding a set of related resources quickly Provide a definitional basis for applying access control to resources quickly Note: At present, compartment definitions can only be defined by HL7 International. This is because their existence creates significant impact on the behavior of servers. **Relationships** Compartment definitions describe how particular compartment instances are named and identified, and how systems know which resources are in the compartment. The following compartments are defined by this specification: Title | Description | Identity | Membership Patient | The set of resources associated with a particular patient | There is an instance of the patient compartment for each patient resource, and the identity of the compartment is the same as the patient. When a patient is linked to another patient, all the records associated with the linked patient are in the compartment associated with the target of the link. | The patient compartment includes any resources where the subject of the resource is the patient, and some other resources that are directly linked to resources in the patient compartment Encounter | The set of resources associated with a particular encounter | There is an instance of the encounter compartment for each encounter resource, and the identity of the compartment is the same as the encounter | The encounter compartment includes any resources where the resource has an explicitly nominated encounter, and some other resources that themselves link to resources in the encounter compartment. Note that for many resources, the exact nature of the link to encounter can be ambiguous (e.g. for a DiagnosticReport, is it the encounter when it was initiated, or when it was reported?) RelatedPerson | The set of resources associated with a particular 'related person' | There is an instance of the relatedPerson compartment for each relatedPerson resource, and the identity of the compartment is the same as the relatedPerson | The relatedPerson compartment includes any resources where the resource is explicitly linked to relatedPerson (usually as author) Practitioner | The set of resources associated with a particular practitioner | There is an instance of the practitioner compartment for each Practitioner resource, and the identity of the compartment is the same as the Practitioner | The practitioner compartment includes any resources where the resource is explicitly linked to a Practitioner (usually as author, but other kinds of linkage exist) Device | The set of resources associated with a particular device | There is an instance of the device compartment for each Device resource, and the identity of the compartment is the same as the Device | The device compartment includes any resources where the resource is explicitly linked to a Device (mostly subject or performer) The full definitions of these compartments are published as CompartmentDefinition resources. Servers typically do not support the full definition of a compartment, and are not required to. Systems may publish CompartmentDefinition resources so that other systems may make use of compartments properly. CompartmentDefinitions are used by CapabilityStatement instances for specifying how resources are accessed **Referenced By** CapabilityStatement --- ## Composition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/composition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A set of healthcare-related information that is assembled together into a single logical package that provides a single coherent statement of meaning, establishes its own context and that has clinical attestation with regard to who is making the statement. A Composition defines the structure and narrative content necessary for a document. However, a Composition alone does not constitute a document. Rather, the Composition must be the first entry in a Bundle where Bundle.type=document, and any other resources referenced from Composition must be included as subsequent entries in the Bundle (for example Patient, Practitioner, Encounter, etc.). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A Composition is the basic structure from which FHIR Documents - immutable bundles with attested narrative - are built. A single logical composition may be associated with a series of derived documents, each of which is a frozen copy of the composition. Note: EN 13606 uses the term "Composition" to refer to a single commit to an EHR system, and offers some common examples: a composition containing a consultation note, a progress note, a report or a letter, an investigation report, a prescription form or a set of bedside nursing observations. Using Composition for an attested EHR commit is a valid use of the Composition resource, but for FHIR purposes, it would be usual to make more granular updates with individual provenance statements. The Clinical Document profile constrains Composition to specify a clinical document (matching CDA ). See also the comparison with CDA. **Relationships** Composition is a structure for grouping information for purposes of persistence and attestability. There are several other grouping structures in FHIR with distinct purposes: The List resource - enumerates a flat collection of resources and provides features for managing the collection. While a particular List instance may represent a "snapshot", from a business process perspective, the notion of "list" is dynamic – items are added and removed over time. The List resource references other resources. Lists may be curated and have specific business meaning. The Group resource - defines a group of specific people, animals, devices, etc. by enumerating them, or by describing qualities that group members have. The Group resource refers to other resources, possibly implicitly. Groups are intended to be acted upon or observed as a whole (e.g., performing therapy on a group, calculating risk for a group, etc.). This resource will commonly be used for public health (e.g., describing an at-risk population), clinical trials (e.g., defining a test subject pool) and similar purposes. The Bundle resource - is an infrastructure container for a group of resources. It does not have narrative and is used to group collections of resources for transmission, persistence or processing (e.g., messages, documents, transactions, query responses, etc.). The content of bundles is typically algorithmically determined for a particular exchange or persistence purpose. The Composition resource - defines a set of healthcare-related information that is assembled together into a single logical document that provides a single coherent statement of meaning, establishes its own context and that has clinical attestation with regard to who is making the statement. The Composition resource provides the basic structure of a FHIR document. The full content of the document is expressed using a Bundle containing the Composition and its entries. The Composition resource organizes clinical and administrative content into sections, each of which contains a narrative, and references other resources for supporting data. The narrative content of the various sections in a Composition are supported by the resources referenced in the section entries. The complete set of content to make up a document includes the Composition resource together with various resources pointed to or indirectly connected to the Composition, all gathered together into a Bundle for transport and persistence. Resources associated with the following list of Composition references SHALL be included in the Bundle: Composition.subject Composition.encounter Composition.author Composition.attester.party Composition.custodian Composition.event.detail Composition.section.author Composition.section.focus Composition.section.entry Other resources referred to by those resources MAY be included in the Bundle at the discretion of the authoring system as documented in the system's operation definition (such as $document operation), or as specified by any applicable profiles. **Background and Context** Composition Status Codes Every composition has a status element, which describes the status of the content of the composition, taken from this list of codes: The workflow/clinical status of the composition. preliminary | This is a preliminary composition or document (also known as initial or interim). The content may be incomplete or unverified. final | This version of the composition is complete and verified by an appropriate person and no further work is planned. Any subsequent updates would be on a new version of the composition. amended | The composition content or the referenced resources have been modified (edited or added to) subsequent to being released as "final" and the composition is complete and verified by an authorized person. entered-in-error | The composition or document was originally created/issued in error, and this is an amendment that marks that the entire series should not be considered as valid. Composition status generally only moves down through this list - it moves from preliminary to final and then it may progress to amended. Note that in many workflows, only final compositions are made available and the preliminary status is not used. A very few compositions are created entirely in error in the workflow - usually the composition concerns the wrong patient or is written by the wrong author, and the error is only detected after the composition has been used or documents have been derived from it. To support resolution of this case, the composition is updated to be marked as entered-in-error and a new derived document can be created. This means that the entire series of derived documents is now considered to be created in error and systems receiving derived documents based on retracted compositions SHOULD remove data taken from earlier documents from routine use and/or take other appropriate actions. Systems are not required to provide this workflow or support documents derived from retracted compositions, but they SHALL NOT ignore a status of entered-in-error. Note that systems that handle compositions or derived documents and don't support the error status need to define some other way of handling compositions that are created in error; while this is not a common occurrence, some clinical systems have no provision for removing erroneous information from a patient's record, and there is no way for a user to know that it is not fit for use - this is not safe. Note for CDA aware readers Many users of this specification are familiar with the Clinical Document Architecture (CDA) and related specifications. CDA is a primary design input to the Composition resource (other principal inputs are other HL7 document specifications and EN13606). There are three important structural differences between CDA and the Composition resource: A composition is a logical construct- its identifier matches to the CDA ClinicalDocument.setId. Composition resources are wrapped into Document structures, for exchange of the whole package (the composition and its parts), and this wrapped, sealed entity is equivalent to a CDA document, where the Bundle.id is equivalent in function to ClinicalDocument.id (but it is not identical when interconverting, since it's a transform between them). The composition section defines a section (or sub-section) of the document, but unlike CDA, the section entries are actually references to other resources that hold the supporting data content for the section. This design means that the data can be reused in many other ways. Unlike CDA, the context defined in the Composition (the confidentiality, subject, author, event, event period and encounter) apply to the composition and do not specifically apply to the resources referenced from the section.entry. There is no context flow model in FHIR, so each resource referenced from within a Composition expresses its own individual context. In this way, clinical content can safely be extracted from the composition. In addition, note that both the code lists (e.g., Composition.status) and the Composition resource are mapped to HL7 v3 and/or CDA. **Referenced By** Contract Procedure --- ## ConceptMap import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/conceptmap.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A statement of relationships from one set of concepts to one or more other concepts - either concepts in code systems, or data element/data element concepts, or classes in class models. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A concept map defines a mapping from a set of concepts defined in a code system (commonly referred to as the "system") to one or more concepts defined in other code systems. In the mapping context, a system can be a typical code system based on a recognized standard or local terminology (in any of its forms), or in some cases it may be an "implicit" code system that is not based on a recognized terminology but still represents a set of "concepts" that can be usefully mapped. Mappings are one way - from the source to the target system. In many cases, the reverse mappings are valid, but this cannot be assumed to be the case. Mappings between code system concepts are only intended to be defined in the context of a particular business usage. The business use case is normally defined by the specification of the source and target value sets. The mappings may be useful in other contexts, but this must be determined based on the context of use and meaning; it cannot be taken for granted automatically. An example where the usage context is important for choosing the correct mapping is mapping from a clinical terminology (e.g. SNOMED CT) to a classification (e.g. ICD-10) for either data analysis or billing. Mappings in the data analysis context would be targeted for an appropriate classification (often at a higher level), whereas in the billing context there may be specific requirements to be met (e.g. leaf level codes only) that could result in multiple mappings for a single source concept and then require additional information beyond the source concept itself in order to select the correct final mapping. Note that all code systems (explicit or implicit) represented in FHIR have URI identifiers for value sets (also either explicit or implicit) that include the entire code system, and these "all codes" value sets can be used for mappings that are valid in all use contexts that are appropriate for the code system. Each mapping for a concept from the source to target system includes an equivalence property that specifies how similar the mapping is (or, in some cases, that there is no valid mapping). There is one element for each concept or field in the source value set or system that needs to be mapped. Each source concept may have multiple targets: because there are multiple possible mappings (e.g., ambiguous) to specify a correct map, and specify other mappings as invalid when there are multiple mappings depending on the values of other elements (dependsOn) There SHOULD be at least one target for each element, but some incomplete concept maps might not have a target for each concept. A key concept for the ConceptMap resource is the $translate operation. This operation is a formal definition of an API by which a terminology server can allow clients to ask for a translation to be done based on the content in the ConceptMap resource. As such it also provides useful perspective on the operational use of ConceptMap resources in any context. **Relationships** While ConceptMap resources are not referred to directly from any other resource, they may be included and used in ImplementationGuide resources, and provide background knowledge that is useful in many contexts, including operations defined in this specification. In addition to ConceptMap, there is also the StructureMap resource. The ConceptMap resource defines relationships between concepts in their own right, along with grading of their equivalencies, while the StructureMap defines an executable transform for instances that conform to a known structure. Both Code System supplements and Concept Maps may be used to define relationships between concepts in different systems. ConceptMaps are assertions of the relationships between different concepts that are associated with particular contexts of use, while CodeSystem supplements are used to define inherent properties and semantics of the concepts in the code system **Background and Context** Further discussion of the issues involved in mapping between concept definition systems can be found in the HL7 v3 Core Principles document and the functionality described in the OMG CTS 2 specification. --- ## Condition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/condition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A clinical condition, problem, diagnosis, or other event, situation, issue, or clinical concept that has risen to a level of concern. Refer to the [US Core Condition profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-condition.html) and the US Core [Condition Encounter](http://hl7.org/fhir/us/core/StructureDefinition-us-core-condition-encounter-diagnosis.html) profile. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Condition is one of the event resources in the FHIR workflow specification. This resource is used to record detailed information about a condition, problem, diagnosis, or other event, situation, issue, or clinical concept that has risen to a level of concern. The condition could be a point in time diagnosis in context of an encounter, it could be an item on the practitioner’s Problem List, or it could be a concern that doesn’t exist on the practitioner’s Problem List. Often times, a condition is about a clinician's assessment and assertion of a particular aspect of a patient's state of health. It can be used to record information about a disease/illness identified from application of clinical reasoning over the pathologic and pathophysiologic findings (diagnosis), or identification of health issues/situations that a practitioner considers harmful, potentially harmful and may be investigated and managed (problem), or other health issue/situation that may require ongoing monitoring and/or management (health issue/concern). The condition resource may be used to record a certain health state of a patient which does not normally present a negative outcome, e.g. pregnancy. The condition resource may be used to record a condition following a procedure, such as the condition of Amputee-BKA following an amputation procedure. While conditions are frequently a result of a clinician's assessment and assertion of a particular aspect of a patient's state of health, conditions can also be expressed by the patient, related person, or any care team member. A clinician may have a concern about a patient condition (e.g. anorexia) that the patient is not concerned about. Likewise, the patient may have a condition (e.g. hair loss) that does not rise to the level of importance such that it belongs on a practitioner’s Problem List. For example, each of the following conditions could rise to the level of importance such that it belongs on a problem or concern list due to its direct or indirect impact on the patient’s health. These examples may also be represented using other resources, such as FamilyMemberHistory, Observation, or Procedure. Unemployed Without transportation (or other barriers) Susceptibility to falls Exposure to communicable disease Family History of cardiovascular disease Fear of cancer Cardiac pacemaker Amputee-BKA Risk of Zika virus following travel to a country Former smoker Travel to a country planned (that warrants immunizations) Motor Vehicle Accident Patient has had coronary bypass graft **Relationships** The condition resource may be referenced by other resources as "reasons" for an action (e.g. MedicationRequest, Procedure, ServiceRequest, etc.) This resource is not typically used to record information about subjective and objective information that might lead to the recording of a Condition resource. Such signs and symptoms are typically captured using the Observation resource; although in some cases a persistent symptom, e.g. fever, headache may be captured as a condition before a definitive diagnosis can be discerned by a clinician. By contrast, headache may be captured as an Observation when it contributes to the establishment of a meningitis Condition. Use the Observation resource when a symptom is resolved without long term management, tracking, or when a symptom contributes to the establishment of a condition. Use Condition when a symptom requires long term management, tracking, or is used as a proxy for a diagnosis or problem that is not yet determined. When the diagnosis is related to an allergy or intolerance, the Condition and AllergyIntolerance resources can both be used. However, to be actionable for decision support, using Condition alone is not sufficient as the allergy or intolerance condition needs to be represented as an AllergyIntolerance. **Referenced By** AdverseEvent Appointment CarePlan CareTeam Claim ClinicalImpression Communication CommunicationRequest Contract CoverageEligibilityRequest DeviceRequest DeviceUseStatement Encounter EpisodeOfCare ExplanationOfBenefit FamilyMemberHistory Goal GuidanceResponse ImagingStudy Immunization MedicationAdministration MedicationRequest MedicationStatement Procedure RequestGroup RiskAssessment ServiceRequest SupplyRequest --- ## Consent import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/consent.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A record of a healthcare consumer’s choices, which permits or denies identified recipient(s) or recipient role(s) to perform one or more actions within a given policy context, for specific purposes and periods of time. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The purpose of this Resource is to be used to express a Consent regarding Healthcare. There are four anticipated uses for the Consent Resource, all of which are written or verbal agreements by a healthcare consumer [grantor] or a personal representative, made to an authorized entity [grantee] concerning authorized or restricted actions with any limitations on purpose of use, and handling instructions to which the authorized entity must comply: Privacy Consent Directive: Agreement to collect, access, use or disclose (share) information. Medical Treatment Consent Directive: Consent to undergo a specific treatment (or record of refusal to consent). Research Consent Directive: Consent to participate in research protocol and information sharing required. Advance Care Directives: Consent to instructions for potentially needed medical treatment (e.g. DNR). This resource is scoped to cover all four uses, but at this time, only the privacy use case is modeled. The scope of the resource may change when the other possible scopes are investigated, tested, or profiled. A FHIR Consent Directive instance is considered the encoded legally binding Consent Directive if it meets requirements of a policy domain requirements for an enforceable contract. In some domains, electronic signatures of one or both of the parties to the content of an encoded representation of a Consent Form is deemed to constitute a legally binding Consent Directive. Some domains accept a notary’s electronic signature over the wet or electronic signature of a party to the Consent Directive as the additional identity proofing required to make an encoded Consent Directive legally binding. Other domains may only accept a wet signature or might not require the parties’ signatures at all. Whatever the criteria are for making an encoded FHIR Consent Directive legally binding, anything less than a legally binding representation of a Consent Directive must be identified as such, i.e., as a derivative of the legally binding Consent Directive, which has specific usage in Consent Directive workflow management. Definitions: Consent | The record of a healthcare consumer’s policy choices, which permits or denies identified recipient(s) or recipient role(s) to perform one or more actions within a given policy context, for specific purposes and periods of time Consent Directive | The legal record of a healthcare consumer's agreement with a party responsible for enforcing the consumer’s choices, which permits or denies identified actors or roles to perform actions affecting the consumer within a given context for specific purposes and periods of time Consent Form | Human readable consent content describing one or more actions impacting the grantor for which the grantee would be authorized or prohibited from performing. It includes the terms, rules, and conditions pertaining to the authorization or restrictions, such as effective time, applicability or scope, purposes of use, obligations and prohibitions to which the grantee must comply. Once a Consent Form is “executed” by means required by policy, such as verbal agreement, wet signature, or electronic/digital signature, it becomes a legally binding Consent Directive. Consent Directive Derivative | Consent Content that conveys the minimal set of information needed to manage Consent Directive workflow, including providing Consent Directive content sufficient to: Represent a Consent Directive Register or index a Consent Directive Query and respond about a Consent Directive Retrieve a Consent Directive Notify authorized entities about Consent Directive status changes Determine entities authorized to collect, access, use or disclose information about the Consent Directive or about the information governed by the Consent Directive. Derived Consent content includes the Security Labels encoding the applicable privacy and security policies. Consent Security Labels inform recipients about specific access control measures required for compliance. Consent Statement | A Consent Directive derivative has less than full fidelity to the legally binding Consent Directive from which it was "transcribed". It provides recipients with the full content representation they may require for compliance purposes, and typically include a reference to or an attached unstructured representation for recipients needing an exact copy of the legal agreement. Consent Registration | The legal record of a healthcare consumer's agreement with a party responsible for enforcing the consumer’s choices, which permits or denies identified actors or roles to perform actions affecting the consumer within a given context for specific purposes and periods of timeA Consent Directive derivative that conveys the minimal set of information needed to register an active and revoked Consent Directive, or to update Consent status as it changes during its lifecycle. Consent Query/Response Types | The FHIR Consent Resource specifies multiple Consent Search parameters, which support many types of queries for Consent Resource content. There are several Query/Response patterns that are typically used for obtaining information about consent directive content for the following use cases: Find Active Consent Directive: A query that includes sufficient consent directive content to determine whether a specific party is authorized to share information governed by a consent directive with another specific party. The Response is either: “Yes” meaning that both parties are authorized to share the information with one another. “No” meaning that the authorized querier is not permitted to share with another specific party “No information found” meaning that there is no active Consent Directive in which the querier is authorized to share the governed information. Find Consent Directive Authorized Entities: A query that includes sufficient consent directive content to return a list of entities with which the querier is authorized to share governed information. The response to an authorized querier is the list of any authorized entities with which the querier is permitted to share governed information. The response to an unauthorized querier is that “no information is found”. Find Consent Directive(s): A query that includes sufficient consent directive content to return a list of Consent Directive metadata for an authorized querier to determine what Consent Directives are available, and to locate and retrieve one or more of those Consent Directives as needed. Policy context | Any organizational or jurisdictional policies, which may limit the consumer’s policy choices, and which includes the named range of actions allowed Healthcare Consumer | The individual establishing his/her personal consent (i.e. Consenter). In FHIR, this is referred to as the 'Patient' though this word is not used across all contexts of care Privacy Consent Directive (PCD) Privacy policies define how Individually Identifiable Health Information (IIHI) is to be collected, accessed, used and disclosed. A Privacy Consent Directive as a legal record of a patient's (e.g. a healthcare consumer) agreement with a party responsible for enforcing the patient's choices, which permits or denies identified actors or roles to perform actions affecting the patient within a given context for specific purposes and periods of time. All consent directives have a policy context, which is any set of organizational or jurisdictional policies which may limit the consumer’s policy choices, and which include a named range of actions allowed. In addition, Privacy Consent Directives provide the ability for a healthcare consumer to delegate authority to a Substitute Decision Maker who may act on behalf of that individual. Alternatively, a consumer may author/publish their privacy preferences as a self-declared Privacy Consent Directive. The Consent resource on FHIR provides support for alternative representations for expressing interoperable health information privacy consent directives in a standard form for the exchange and enforcement by sending, intermediating, or receiving systems of privacy policies that can be enforced by consuming systems (e.g., scanned documents, of computable structured entries elements, FHIR structures with optional attached, or referenced unstructured representations.) It may be used to represent the Privacy Consent Directive itself, a Consent Statement, which electronically represents a Consent Directive, or Consent Metadata, which is the minimum necessary consent content derived from a Consent Directive for use in workflow management. **Relationships** Consent management - particularly privacy consent - is complicated by the fact that consent to share is often itself necessary to protect. The need to protect the privacy of the privacy statement itself competes with the execution of the consent statement. For this reason, it is common to deal with 'consent statements' that are only partial representations of the full consent statement that the patient provided. For this reason, the consent resource contains two elements that refer back to the source: a master identifier, and a direct reference to content from which this Consent Statement was derived. That reference can be one of several things: A reference to another consent resource from which this limited statement was derived A reference to a document format for the original source (e.g. PDF or CDA - see the HL7 CDAR2 ConsentDirective Implementation Guide , which incorporated the IHE Basic Patient Privacy Consents (BPPC) ), either directly, or in a reference The source can be included in the consent as an attachment The consent statements represent a chain that refers back to the original source consent directive. Applications may be able to follow the chain back to the source but should not generally assume that they are authorized to do this. Consent Directives are executed by verbal acknowledge or by being signed - either on paper, or digitally. Consent Signatures will be found in the Provenance resource (example consent and signature). Implementation Guides will generally make rules about what signatures are required, and how they are to be shared and used. **Background and Context** Change to "The Consent resource is structured with a base policy (represented as Consent.policy/Consent.policyRule) which is either opt-in or opt-out, followed by a listing of exceptions to that policy (represented as Consent.provision(s)). The exceptions can be additional positive or negative exceptions upon the base policy. The set of exceptions include a list of data objects, list of authors, list of recipients, list of Organizations, list of purposeOfUse, and Date Range. The enforcement of the Privacy Consent Directive is not included but is expected that enforcement can be done using a mix of the various Access Control enforcement methodologies (e.g. OAuth, UMA, XACML). This enforcement includes the details of the enforcement meaning of the elements of the Privacy Consent Directive, such as the rules in place when there is an opt-in consent would be specific about which organizational roles have access to what kinds of resources (e.g. RBAC, ABAC). The specification of these details is not in scope for the Consent resource. **Referenced By** ResearchSubject --- ## Contract import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/contract.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Legally enforceable, formally recorded unilateral or bilateral directive i.e., a policy or agreement. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource allows for the instantiation of various types of legally enforceable agreements or policies as shareable, consumable, and executable artifacts as well as precursory content upon which instances may be based or derivative artifacts supporting management of their basal instance. The resource is general enough to encode broad range of legal artifacts such as: A Contract instance, which may undergo legal state transitions during its formation. Templates for or agreed upon instances of rules, obligations, and prohibitions. An organizational or jurisdictional policy, for example, for establishing rules of conduct or obligations necessary to achieve societal goods. Or it may encode a contract instance, which may be accepted by one party in exchange of considerations offered by another party. A derivative of a contract instance for purposes of workflow management of the agreement, e.g., for retrieval or indication of agreed upon terms, such as entities allowed to exchange governed assets. Contracts are promises or understandings that are enforceable by law in case of any violation or breech by any involved party or organization. A Contract instance may be a unilateral mandate such as a policy, or a unilateral, bilateral, or multilateral agreement, which impacts the influence of the parties over the terms of the agreement, and the burdens and risks incurred. Contract Resource may be typed to support multiple types of domain policies and contractual agreements, and specializations of those types. A Contract instance must include at least one term with an offer, which obligates the offerer to or not to do, perform, or effectuate some action in exchange for some consideration in return from the offeree, e.g., another obligatory action or non-action, or an asset. The Contract action element follows the Request Pattern to describe the proposal, plan, or order to effectuate the obligatory service or activity specified in a term’s offer. The Contract action may reference and specify the roles for one or more target entities, the requesters, and performers. By following the Request Pattern, the Contract provides the criteria needed to assess whether the contract obligations have been enforced, for example, in the case of a privacy policy, by an access control system. The Contract asset element supports detailed description of the consideration being exchanged in a Contract instance or the satisfaction of a policy imperative such as the obligation to render aid as required by local law. The Contract valuedItem element supports detailed description of the monetary worth of a Contract asset being marketed, the price of products being sold, or the property taxes required under a jurisdictional tax law. A Contract may be used as a content derivative, which contains the minimal content derived from the basal information source at a specific stage in its lifecycle, which is needed for management and communications about the basal information source. For example, the metadata used to register a Contract’s characteristics and retrieval address in a federated registry/repository exchange ecosystem. In addition to other uses of derivatives, the Contract Resource may function simply as the computable representation of the executed contract, which may be the attached to the Contract Resource as the 'legally binding' scanned paper contract attachment or referenced location, or as the 'friendly' electronic form such as an html page or a QuestionnaireResponse. By using the Contract linkID elements, which are associated with key Contract elements, a Contract Resource may be automatically populated with the values expressed in a related QuestionnaireResponse. Note that the Contract Resource may be considered the legally binding contract only if it is intended to be the sole 'executed' encoding of this contract, and includes the legally binding signatures. I.e., even if the Contract Resource is populated based on content in a hard-copy contract or an electronic contract form intended to collect both the content and the signature of relevant parties to the contract, if contracting parties have agreed or acknowledged that the Contract Resource conveys the binding and enforceable legal contract and that it is fully traceable to the forms used to collect its content, meeting the legal concept of being within the 'four corners of a contract', i.e., that the meaning of the contract, will, or deed is represented solely by this instance of the Contract Resource. This usage could be implemented with digital ledger technology to form a 'smart contract' to the extent that an instance supports elements critical to computable algorithms intended to achieve some output. Where the Consent resource applies, the Consent resource should be used. Where a Contract exists for a consent directive then if a Consent for this also exists the Consent shall reference the Contract. **Relationships** Contracts are used to record in machine processible form a legal agreement between parties **Background and Context** Implementers should be familiar with legal concepts, Ricardian Contracts and have a general knowledge of recording agreements. Legal State Machine This Resource supports tracking of the progress of a Contract instance during its lifecycle as a 'legal instrument' from inception as a draft, possibly based on a definitional contract template to negotiations and the various permutation on term elements that may occur, on to execution. Then it follows the Contract as enforceable obligations, which may be breached, disputed, or modified, until the Contract reaches renewal, termination, or revocation. This is flow is orthogonal to the Contract.status, which tracks the progress of the documentation of the Contract whether it is definitional, a derivative, or an instance. The legal state value set specifies the characteristics of these states based on legal definitions. **Referenced By** Consent Coverage SupplyDelivery --- ## Coverage import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/coverage.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Financial instrument which may be used to reimburse or pay for health care products and services. Includes both insurance and self-payment. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The Coverage resource is intended to provide the high-level identifiers and descriptors of an insurance plan, typically the information which would appear on an insurance card, which may be used to pay, in part or in whole, for the provision of health care products and services. This resource may also be used to register 'SelfPay' where an individual or organization other than an insurer is taking responsibility for payment for a portion of the health care costs. Selfpay should not be confused with being a guarantor of the patient's account. The Coverage resource is a "event" resource from a FHIR workflow perspective - see Workflow Request. **Relationships** The eClaim domain includes a number of related insurance resources Coverage | The Coverage resource is intended to provide the high-level identifiers and descriptors of a specific insurance plan for a specific individual - essentially the insurance card information. This may alternately provide the individual or organization, selfpay, which will pay for products and services rendered. Contract | A Contract resource holds the references to parties who have entered into an agreement of some type, the parties who may sign or witness such an agreement, descriptors of the type of agreement and even the actual text or executable copy of the agreement. The agreement may be of a variety of types including service contracts, insurance contracts, directives, etc. The contract may be either definitional or actual instances. InsurancePlan | The InsurancePlan resource holds the definition of an insurance plan which an insurer may offer to potential clients through insurance brokers or an online insurance marketplace. This is only the plan definition and does not contain or reference a list of individuals who have purchased the plan. **Referenced By** Account Claim ClaimResponse CoverageEligibilityRequest CoverageEligibilityResponse DeviceRequest EnrollmentRequest ExplanationOfBenefit MedicationRequest ServiceRequest Task --- ## CoverageEligibilityRequest import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/coverageeligibilityrequest.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The CoverageEligibilityRequest provides patient and insurance coverage information to an insurer for them to respond, in the form of an CoverageEligibilityResponse, with information regarding whether the stated coverage is valid and in-force and optionally to provide the insurance details of the policy. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The CoverageEligibilityRequest makes a request of an insurer asking them to provide, in the form of an CoverageEligibilityResponse, information regarding: (validation) whether the specified coverage(s) is valid and in-force; (discovery) what coverages the insurer has for the specified patient; (benefits) the benefits provided under the coverage; whether benefits exist under the specified coverage(s) for specified classes of services and products; and (auth-requirements) whether preauthorization is required, and if so what information may be required in that preauthorization, for the specified service classes or services. The CoverageEligibilityRequest resource is a "event" resource from a FHIR workflow perspective - see Workflow Event. Additional Information Additional information regarding electronic coverage eligibility content and usage may be found at: Financial Resource Status Lifecycle: how .status is used in the financial resources. Subrogation: how eClaims may handle patient insurance coverages when another insurer rather than the provider will settle the claim and potentially recover costs against specified coverages. Coordination of Benefit: how eClaims may handle multiple patient insurance coverages. Batches: how eClaims may handle batches of eligibility, claims and responses. Attachments and Supporting Information: how eClaims may handle the provision of supporting information, whether provided by content or reference, within the eClaim resource when submitted to the payor or later in a resource which refers to the subject eClaim resource. This includes how payors how request additional supporting information from providers. **Relationships** CoverageEligibilityRequest should be used when requesting whether the patient's coverage is inforce, whether it is valid at this or a specified date, or requesting the benefit details or preauthorization requirements associated with a coverage. The Claim resource should be used to request the adjudication and/or authorization of a set of healthcare-related goods and services for a patient against the patient's insurance coverages, or to request what the adjudication would be for a supplied set of goods or services should they be actually supplied to the patient. The Coverage resource contains the information typically found on the health insurance card for an individual used to identify the covered individual to the insurer and is referred to by the CoverageEligibilityRequest. The eClaim domain includes a number of related resources CoverageEligibilityRequest | Patient and insurance coverage information provided to an insurer for them to respond, in the form of an CoverageEligibilityResponse, with information regarding whether the stated coverage is valid and in-force and optionally to provide the insurance details of the policy. Claim | A suite of goods and services and insurances coverages under which adjudication or authorization is requested. Coverage | Provides the high-level identifiers and descriptors of an insurance plan, typically the information which would appear on an insurance card, which may be used to pay, in part or in whole, for the provision of health care products and services. **Referenced By** CoverageEligibilityResponse --- ## CoverageEligibilityResponse import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/coverageeligibilityresponse.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; This resource provides eligibility and plan details from the processing of an CoverageEligibilityRequest resource. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The CoverageEligibilityResponse resource provides eligibility and plan details from the processing of an CoverageEligibilityRequest resource. It combines key information from a payor as to whether a Coverage is in-force, and optionally the nature of the Policy benefit details as well as the ability for the insurer to indicate whether the insurance provides benefits for requested types of services or requires preauthorization and if so what supporting information may be required. The CoverageEligibilityResponse resource is a "event" resource from a FHIR workflow perspective - see Workflow Event. Additional Information Additional information regarding electronic coverage eligibility content and usage may be found at: Financial Resource Status Lifecycle: how .status is used in the financial resources. Subrogation: how eClaims may handle patient insurance coverages when another insurer rather than the provider will settle the claim and potentially recover costs against specified coverages. Coordination of Benefit: how eClaims may handle multiple patient insurance coverages. Batches: how eClaims may handle batches of eligibility, claims and responses. Attachments and Supporting Information: how eClaims may handle the provision of supporting information, whether provided by content or reference, within the eClaim resource when submitted to the payor or later in a resource which refers to the subject eClaim resource. This includes how payors how request additional supporting information from providers. **Relationships** CoverageEligibilityResponse should be used to respond to a request on whether the patient's coverage is inforce, whether it is valid at this or a specified date, or to report the benefit details or preauthorization requirements associated with a coverage. When requesting whether the patient's coverage is inforce, whether it is valid at this or a specified date, or requesting the benefit details or preauthorization requirements associated with a coverage CoverageEligibilityRequest should be used instead. The ClaimResponse resource is an insurer's adjudication and/or authorization of a set of healthcare-related goods and services for a patient against the patient's insurance coverages. For reporting out to patients or transferring data to patient centered applications, such as patient health Record (PHR) application, the ExplanationOfBenefit should be used . The Coverage resource contains the information typically found on the health insurance card for an individual used to identify the covered individual to the insurer and is referred to by the CoverageEligibilityResponse. The eClaim domain includes a number of related resources CoverageEligibilityResponse | Patient and insurance coverage information provided to an insurer for them to respond, in the form of an CoverageEligibilityResponse, with information regarding whether the stated coverage is valid and in-force and optionally to provide the insurance details of the policy. ClaimResponse | A payor's adjudication and/or authorization response to the suite of services provided in a Claim. Typically the ClaimResponse references the Claim but does not duplicate the clinical or financial information provided in the claim. CoverageEligibilityRequest | Patient and insurance coverage information provided to an insurer for them to respond, in the form of an CoverageEligibilityResponse, with information regarding whether the stated coverage is valid and in-force and optionally to provide the insurance details of the policy. Coverage | Provides the high-level identifiers and descriptors of an insurance plan, typically the information which would appear on an insurance card, which may be used to pay, in part or in whole, for the provision of health care products and services. ExplanationOfBenefit | This resource combines the information from the Claim and the ClaimResponse, stripping out any provider or payor proprietary information, into a unified information model suitable for use for: patient reporting; transferring information to a Patient Health Record system; and, supporting complete claim and adjudication information exchange with regulatory and analytics organizations and other parts of the provider's organization. --- ## DetectedIssue import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/detectedissue.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Indicates an actual or potential clinical issue with or between one or more active or proposed clinical actions for a patient; e.g. Drug-drug interaction, Ineffective treatment frequency, Procedure-condition conflict, etc. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is an event resource from a FHIR workflow perspective - see Workflow, specifically Event. This resource applies to various circumstances where there is a concern about an existing or proposed set of clinical activity. The issue could relate to single, proposed, or multiple actions. It does not apply to technical issues (e.g. lack of user permissions) but could relate to violation of patient consent limitations. Examples include: Drug-drug interactions Inappropriate therapy (wrong dose, frequency, body site) Duplicate therapy This resource represents a specific instance of a potential issue for a particular patient. It is not intended to represent general patient-independent knowledge. This resource is also not intended to be used in defining general prohibitions on actions such as "No NSAIDs", "No solid oral dose forms" or "No MRIs - metallic tattoos". These guidelines can be captured using the AllergyIntolerance, and/or Flag resources. Similarly, this resource is not to be used to capture clinical facts that may imply contraindications such as pregnancy, breast feeding, patient preferences, past procedures, etc. These would be represented using Condition, Procedure or other resources. **Relationships** This resource only applies to documenting a risk associated with a specific planned or ongoing action, or the lack of an action which should be planned - not a general propensity to risk. The latter would be handled using AllergyIntolerance for substance-specific issues or Flag for other types of issues. In addition, the resource represents patient-specific and time-bound risk manifestations, not generic knowledge statements about risks that can exist. This resource is limited to clinical issues associated with a proposed or ongoing action. It does not cover technical issues such as lack of permission, duplicate identifiers, insufficient data, and other business rule violations. Technical issues are conveyed using the OperationOutcome resource. It is possible to have both OperationOutcome and DetectedIssue together, where the OperationOutcome might indicate that a requested action was rejected due to a clinical issue and the DetectedIssue provides the details of the issue. **Background and Context** Detected issues are typically identified by decision support systems. However, they may also be captured directly by clinicians. The latter typically happens for one of two reasons: A clinician wishes to communicate an issue to another clinician whose responsibility would be to resolve it (e.g. a pharmacist identifying an issue with a prescription prior to putting it on hold) A clinician wishes to pre-emptively identify that an issue is known and is being managed (to avoid red flags being raised as part of downstream workflow); e.g. Submitting a new order and including a link to a "duplicate therapy" issue with mitigation indicating that the therapy is not considered to be duplicate. Decision-support generated issues can result from calling a decision-support engine directly (e.g. via a custom OperationDefinition) or as part of an attempt to perform some other function (creating an order, submitting an insurance claim, capturing a medication list). When the issues are generated as a by-product of performing some other sort of action, they may be included in the "response" to the requested action in the same manner as an OperationOutcome. In fact, both may be present - the OperationOutcome indicating that there was a warning or error associated with the request and a DetectedIssue providing the clinical details. (The OperationOutcome could point to the DetectedIssue via an extension.) In those circumstances where requested operations are rejected as a result of a detected issue, the workflow may support allowing the operation to be re-tried, provided that the identified issue is included as part of the submission (possibly also including a mitigation). In doing so, the sender acknowledges the issue and takes responsibility for it, thus allowing the requested operation to proceed. See Linking to Detected Issues for guidance on how a DetectedIssue instance might be included as part of another operation. Systems that require such workflows should document expected behavior as part of their CapabilityStatement declarations. **Referenced By** MedicationDispense MedicationKnowledge MedicationRequest --- ## Device import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/device.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; 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. For more on implantable devices, refer to [US Core Implantable Device Profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-implantable-device.html). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is an administrative resource that tracks individual instances of a device and their location. It is referenced by other resources for recording which device performed an action such as a procedure or an observation, referenced when prescribing and dispensing devices for patient use or for ordering supplies, and used to record and transmit Unique Device Identifier (UDI) information about a device such as a patient's implant. **Relationships** These are the device related resources Device (this resource) DeviceDefinition - Describes a "kind" of device - not a physical instance, cut a "catalog entry" where a device is defined by the manufacturer, reseller, or regulator. DeviceMetric - Describes a measurement, calculation or setting capability of a medical device. In FHIR, the "Device" is the "administrative" resource for the device (it does not change much and has manufacturer information etc.), whereas the DeviceComponent and DeviceMetric (which is really a kind of DeviceComponent) model the physical part, including operation status and is much more volatile. The physical composition of a Device is represented by the Devices pointing to their "parent". Devices differ from medications because they are not "used up" - they remain active in a patient in an ongoing fashion. However, the specific boundary between medications and devices is defined at the implementation level and this standard does not enforce a boundary with the exception of devices that are implanted in a patient. The Medication resource should not be used to represent implanted devices. **Referenced By** Signature Account AdverseEvent Appointment AppointmentResponse AuditEvent CarePlan CatalogEntry ChargeItem ChargeItemDefinition Claim Communication CommunicationRequest Composition Consent Contract DetectedIssue DeviceMetric DeviceRequest DeviceUseStatement DiagnosticReport DocumentManifest DocumentReference ExplanationOfBenefit Flag Group GuidanceResponse ImagingStudy Invoice List MeasureReport Media MedicationAdministration MedicationDispense MedicationRequest MessageHeader MolecularSequence Observation Procedure Provenance QuestionnaireResponse RequestGroup RiskAssessment Schedule ServiceRequest Specimen SupplyDelivery SupplyRequest Task --- ## DeviceDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/devicedefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The characteristics, operational status and capabilities of a medical-related component of a medical device. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The DeviceDefinition resource is used to describe the characteristics and capabilities of a medical device. **Relationships** The DeviceDefinition resource contains the "catalog" definition of a device - whether that definition is authored by the regulatory entities, or it is a local definition that includes assembled device configurations. The DeviceDefinition allows defining hierarchical device configurations (devices as part of other devices). Device vs deviceDefinition: The Device resource is meant to refer to a physical instance of a device - hence having attributes like lot number, patient, location, which the DeviceDefinition resource does not have. **Background and Context** **Referenced By** Device MedicinalProductPackaged MedicinalProductPharmaceutical --- ## DeviceMetric import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/devicemetric.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Describes a measurement, calculation or setting capability of a medical device. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The DeviceMetric resource describes mandatory static properties that characterize a direct or derived, quantitative or qualitative biosignal measurement, setting, or calculation produced by a medical device. The DeviceMetric resource can also be used to describe the non-static but highly relevant properties to the metric such as metric status, metric last calibration time and type, measurement mode, color, reference link to the parent DeviceComponent to where it belongs, and any capabilities that the metric offers (for example: setting the metric label). Note: For the initial scope, this DeviceMetric resource is only applicable to describe a single metric node in the containment tree that is produced by the context scanner in any medical device that implements or derives from the ISO/IEEE 11073 standard. **Relationships** There are two related resources Device - The physical device that this DeviceMetric belongs to. **Referenced By** Media Observation --- ## DeviceRequest import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/devicerequest.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Represents a request for a patient to employ a medical device. The device may be an implantable device, or an external assistive device, such as a walker. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage**
Trial-Use Note: In terms of scope and usage, the Orders and Observations workgroups wish to draw the attention of reviewers and implementers to the following issue: We are seeking input from the implementation community on whether to combine DeviceRequest and VisionPrescription for R4. The parameters element was added to DeviceRequest for representing custom details for ordering devices. With this change, VisionPrescription significantly overlaps DeviceRequest. To illustrate this, the devicerequest-left-lens and devicerequest-right-lens examples are based on the general glasses example in VisionPrescription.
This resource is a request resource from a FHIR workflow perspective - see Workflow. This resource describes the request for the use of a device by a patient. The device may be any pertinent device specified in the Device resource. Examples of devices that may be requested include wheelchair, hearing aids, or an insulin pump. The request may lead to the dispensing of the device to the patient or for use by the patient. The device use request may represent an order or a prescription entered by a practitioner in a CPOE system or a proposal made by a clinical decision support (CDS) system based on a patient's clinical record and context of care. **Relationships** This resource deals with the allocation of a device to a patient and while it may contain instructions on how to use the device, the data about getting the device to the patient is addressed in other resources. For example, certain devices must be implanted via a surgical or other procedure and the implantation or explantation is represented in the Procedure or ServiceRequest resource. The SupplyRequest resource is similar in that it deals with requesting a particular item for a specific patient. However, DeviceRequest is concerned with items where there is a patient focus, or instructions regarding their use, whereas SupplyRequest is concerned with inventory management. **Referenced By** CarePlan Claim Observation --- ## DeviceUseStatement import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/deviceusestatement.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A record of a device being used by a patient where the record is the result of a report from the patient or another clinician. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** These resources have not yet undergone proper review by PC, CQI, CDS, and OO. At this time, they are to be considered only as draft resource proposals for potential submission. This resource is an event resource from a FHIR workflow perspective - see Workflow. It is the intent of the Orders and Observation Workgroup to align this resource with the workflow pattern for event resources. This resource records the use of a healthcare-related device by a patient. The record is the result of a report of use by the patient, another provider or a related person. The resource can be used to note the use of an assistive device such as a wheelchair or hearing aid, a contraceptive such an intra-uterine device, or other implanted devices such as a pacemaker. **Relationships** This resource is different from DeviceRequest which records a request to use the device. This also is distinct from the Procedure resource which may describe the implantation or explantation of a device. --- ## DiagnosticReport import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/diagnosticreport.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The findings and interpretation of diagnostic tests performed on patients, groups of patients, devices, and locations, and/or specimens derived from these. The report includes clinical context such as requesting and provider information, and some mix of atomic results, images, textual and coded interpretations, and formatted representation of diagnostic reports. Refer to the [US Core DiagnosticReport profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-diagnosticreport-note.html) for Report and Note exchange. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is an event resource from a FHIR workflow perspective - see Workflow. It is the intent of the Orders and Observation Workgroup to align this resource with the workflow pattern for event resources. A diagnostic report is the set of information that is typically provided by a diagnostic service when investigations are complete. The information includes a mix of atomic results, text reports, images, and codes. The mix varies depending on the nature of the diagnostic procedure, and sometimes on the nature of the outcomes for a particular investigation. In FHIR, the report can be conveyed in a variety of ways including a Document, RESTful API, or Messaging framework. Included within each of these, would be the DiagnosticReport resource itself. The DiagnosticReport resource has information about the diagnostic report itself, and about the subject and, in the case of laboratory tests, the specimen of the report. It can also refer to the request details and atomic observations details or image instances. Report conclusions can be expressed as a simple text blob, structured coded data or as an attached fully formatted report such as a PDF. The DiagnosticReport resource is suitable for the following kinds of diagnostic reports: Laboratory (Clinical Chemistry, Hematology, Microbiology, etc.) Pathology / Histopathology / related disciplines Imaging Investigations (x-ray, CT, MRI etc.) Other diagnostics - Cardiology, Gastroenterology etc. The DiagnosticReport resource is not intended to support cumulative result presentation (tabular presentation of past and present results in the resource). The DiagnosticReport resource does not yet provide full support for detailed structured reports of sequencing; this is planned for a future release. **Relationships** Note that many diagnostic processes are procedures that generate observations and diagnostic reports. In many cases, such an observation does not require an explicit representation of the procedure used to create the observation, but where there are details of interest about how the diagnostic procedure was performed, the Procedure resource is used to describe the activity. In contrast to the Observation resource, the DiagnosticReport resource typically includes additional clinical context and some mix of atomic results, images, imaging reports, textual and coded interpretation, and formatted representations. Laboratory reports, pathology reports, and imaging reports should be represented using the DiagnosticReport resource. The Observation resource is referenced by the DiagnosticReport to provide the atomic results for a particular investigation. If you have a highly structured report, then use DiagnosticReport - it has data and workflow support. Details about the request for a diagnostic investigation are captured in the various "request" resources (e.g., the ServiceRequest) and allow the report to connect to clinical workflows. For more narrative driven reports with less work flow (histology/mortuary, etc.), the Composition resource would be more appropriate. Image and media representations of the report and supporting images are referenced in the DiagnosticReport resource. The details and actual image instances can be referenced directly in Diagnostic report using the "imaging" element or by indirect reference through the ImagingStudy resources which represent the content produced in a DICOM imaging study or set of DICOM Instances for a patient. **Background and Context** Diagnostic Report Names The words "tests", "results", "observations", "panels" and "batteries" are often used interchangeably when describing the various parts of a diagnostic report. This leads to much confusion. The naming confusion is worsened because of the wide variety of forms that the result of a diagnostic investigation can take, as described above. Languages other than English have their own variations on this theme. This resource uses one particular set of terms. A practitioner "requests" a set of "tests". The diagnostic service returns a "report" which may contain a "narrative" - a written summary of the outcomes, and/or "results" - the individual pieces of atomic data which each are "observations". The results are assembled in "groups" which are nested structures of Observations (traditionally referred to as "panels" or " batteries" by laboratories) that can be used to represent relationships between the individual data items. **Referenced By** CarePlan ChargeItem ClinicalImpression Communication CommunicationRequest Condition Contract DeviceRequest DeviceUseStatement FamilyMemberHistory GuidanceResponse ImagingStudy Immunization MedicationAdministration MedicationStatement Procedure RequestGroup RiskAssessment ServiceRequest SupplyRequest --- ## DocumentManifest import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/documentmanifest.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A collection of documents compiled for a purpose together with metadata that applies to the collection. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A document manifest gathers a set of Resources into a single package with some clinical context that may be the subject of workflow such as targeted delivery. The usage of "document" in the case of DocumentManifest, covers the broader scope of "document" as defined in REST and HTTP. This use is not limited to the formal "Document" model used for FHIR documents or DocumentReference resources. Typically, DocumentManifest resources are used in document indexing systems, such as IHE XDS . Such as profiled in IHE Mobile access to Health Documents . When used as a Manifest of "Documents" in this way, the DocumentManifest will point exclusively to DocumentReference resources. A DocumentManifest resource could be used to bring clinical context grouping to a broader set of Resources. --- ## DocumentReference import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/documentreference.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A reference to a document of any kind for any purpose. Provides metadata about the document so that the document can be discovered and managed. The scope of a document is any seralized object with a mime-type, so includes formal patient centric documents (CDA), clinical notes, scanned paper, and non-patient specific documents like policy text. Refer to the [US Core DocumentReference profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-documentreference.html). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A DocumentReference resource is used to index a document, clinical note, and other binary objects to make them available to a healthcare system. A document is some sequence of bytes that is identifiable, establishes its own context (e.g., what subject, author, etc. can be displayed to the user), and has defined update management. The DocumentReference resource can be used with any document format that has a recognized mime type and that conforms to this definition. Typically, DocumentReference resources are used in document indexing systems, such as IHE XDS , such as profiled in IHE Mobile access to Health Documents . DocumentReference is metadata describing a document such as: CDA documents in FHIR systems FHIR documents stored elsewhere (i.e. registry/repository following the XDS model) PDF documents , Scanned Paper, and digital records of faxes Clinical Notes in various forms Image files (e.g., JPEG, GIF, TIFF) Non-Standard formats (e.g., WORD) Other kinds of documents, such as records of prescriptions or immunizations **Relationships** FHIR defines both a document format and this document reference. FHIR documents are for documents that are authored and assembled in FHIR. This resource is mainly intended for general references to assembled documents. The document that is a target of the reference can be a reference to a FHIR document served by another server, or the target can be stored in the special FHIR Binary Resource, or the target can be stored on some other server system. The document reference is also able to address documents that are retrieved by a service call such as an XDS.b RetrieveDocumentSet, or a DICOM exchange, or an HL7 v2 message query - though the way each of these service calls works must be specified in some external standard or other documentation. A DocumentReference describes some other document. This means that there are two sets of provenance information relevant here: the provenance of the document, and the provenance of the document reference. Sometimes, the provenance information is closely related, as when the document producer also produces the document reference, but in other workflows, the document reference is generated later by other actors. In the DocumentReference resource, the meta content refers to the provenance of the reference itself, while the content described below concerns the document it references. Like all resources, there is overlap between the information in the resource directly, and in the general Provenance resource. This is discussed as part of the description of the Provenance resource. **Referenced By** AdverseEvent CarePlan Communication CommunicationRequest Consent Contract DeviceRequest DeviceUseStatement FamilyMemberHistory GuidanceResponse ImagingStudy MedicationKnowledge MedicinalProduct Observation Procedure RequestGroup RiskAssessment ServiceRequest SubstanceReferenceInformation SubstanceSpecification SupplyRequest --- ## EffectEvidenceSynthesis import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/effectevidencesynthesis.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The EffectEvidenceSynthesis resource describes the difference in an outcome between exposures states in a population where the effect estimate is derived from a combination of research studies. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The scope of the EffectEvidenceSynthesis resource is to describe the estimates of an effect of an exposure on an outcome where the effect estimates are derived from the combination of research studies. Effect estimates are a measure of difference between the exposure state and an alternative exposure state (often called the control or comparator state) Expressing effect estimates is done throughout reporting of biomedical research, systematic reviews, and clinical reference across all disciplines. --- ## Encounter import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/encounter.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; An interaction between a patient and healthcare provider(s) for the purpose of providing healthcare service(s) or assessing the health status of a patient. Refer to the US Core [Encounter](http://hl7.org/fhir/us/core/StructureDefinition-us-core-encounter.html) Profile and the US Core [Condition Encounter](http://hl7.org/fhir/us/core/StructureDefinition-us-core-condition-encounter-diagnosis.html) profile. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A patient encounter is further characterized by the setting in which it takes place. Amongst them are ambulatory, emergency, home health, inpatient and virtual encounters. An Encounter encompasses the lifecycle from pre-admission, the actual encounter (for ambulatory encounters), and admission, stay and discharge (for inpatient encounters). During the encounter the patient may move from practitioner to practitioner and location to location. Because of the broad scope of Encounter, not all elements will be relevant in all settings. For this reason, admission/discharge related information is kept in a separate Hospitalization component within Encounter. The class element is used to distinguish between these settings, which will guide further validation and application of business rules. There is also substantial variance from organization to organization (and between jurisdictions and countries) on which business events translate to the start of a new Encounter, or what level of aggregation is used for Encounter. For example, each single visit of a practitioner during a hospitalization may lead to a new instance of Encounter, but depending on local practice and the systems involved, it may well be that this is aggregated to a single instance for a whole hospitalization. Even more aggregation may occur where jurisdictions introduce groups of Encounters for financial or other reasons. Encounters can be aggregated or grouped under other Encounters using the partOf element. See below for examples. Encounter instances may exist before the actual encounter takes place to convey pre-admission information, including using Encounters elements to reflect the planned start date or planned encounter locations. In this case the status element is set to 'planned'. The Hospitalization component is intended to store the extended information relating to a hospitalization event. It is always expected to be the same period as the encounter itself. Where the period is different, another encounter instance should be used to capture this information as a partOf this encounter instance. The Procedure and encounter have references to each other, and these should be to different procedures; one for the procedure that was performed during the encounter (stored in Procedure.encounter), and another for cases where an encounter is a result of another procedure (stored in Encounter.indication) such as a follow-up encounter to resolve complications from an earlier procedure. Status Management During the life-cycle of an encounter it will pass through many statuses. Typically these are in order or the organization's workflow: planned, in-progress, finished/cancelled. This status information is often used for other things, and often an analysis of the status history is required. This could be done by scanning through all the versions of the encounter, checking the period of each, and then doing some form of post processing. To ease the burden of this (or where a system doesn't support resource histories) a status history component is included. There is no direct indication purely by the status field as to whether an encounter is considered "admitted". The context of the encounter and business practices/policies/workflows/types can influence this definition. (e.g., acute care facility, aged care center, outpatient clinic, emergency department, community-based clinic). Statuses of "arrived", "triaged" or "in progress" could be considered the start of the admission, and also have the presence of the hospitalization sub-component entered. The "on leave" status might or might not be a part of the admission, for example if the patient was permitted to go home for a weekend or some other form of external event. The location is also likely to be filled in with a location status of "present". For other examples such as an outpatient visit (day procedure - colonoscopy), the patient could also be considered to be admitted, hence the encounter doesn't have a fixed definition of admitted. At a minimum, we do believe that a patient IS admitted when the status is in-progress. **Relationships** The Encounter resource is not to be used to store appointment information, the Appointment resource is intended to be used for that. Note that in many systems outpatient encounters (which are in scope for Encounter) and Appointment are used concurrently. In FHIR, Appointment is used for establishing a date for the encounter, while Encounter is applicable to information about the actual Encounter, i.e., the patient showing up. As such, an encounter in the "planned" status is not identical to the appointment that scheduled it, but it is the encounter prior to its actual occurrence, with the expectation that encounter will be updated as it progresses to completion. Patient arrival at a location does not necessarily mean the start of the encounter (e.g. a patient arrives an hour earlier than he is actually seen by a practitioner). An appointment is normally used for the planning stage of an appointment, searching, locating an available time, then making the appointment. Once this process is completed and the appointment is about to start, then the appointment will be marked as fulfilled, and linked to the newly created encounter. This new encounter may start in an "arrived" status when they are admitted at a location of the facility, and then will move to the ward where another part-of encounter may begin. Communication resources are used for a simultaneous interaction between a practitioner and a patient where there is no direct contact. Examples include a phone message, or transmission of some correspondence documentation. There is no duration recorded for a communication resource, but it could contain sent and received times. Standard Extension: Associated Encounter This extension should be used to reference an encounter where there is no property that already defines this association on the resource. **Referenced By** AdverseEvent AllergyIntolerance CarePlan CareTeam ChargeItem Claim ClinicalImpression Communication CommunicationRequest Composition Condition Contract DeviceRequest DiagnosticReport DocumentReference ExplanationOfBenefit Flag GuidanceResponse ImagingStudy Immunization List Media MedicationAdministration MedicationDispense MedicationRequest MedicationStatement NutritionOrder Observation Procedure QuestionnaireResponse RequestGroup RiskAssessment ServiceRequest Task VisionPrescription --- ## Endpoint import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/endpoint.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The technical details of an endpoint that can be used for electronic services, such as for web services providing XDS.b or a REST endpoint for another FHIR server. This may include any security context information. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** An endpoint describes the technical details of a location that can be connected to for the delivery/retrieval of information. Sufficient information is required to ensure that a connection can be made securely, and appropriate data transmitted as defined by the endpoint owner. This is not a description of details of the current system, as found in CapabilityStatement, but of another (potentially external) system. These may be locally hosted services, regional services, or national service. These resources are typically used to identify where to locate endpoint details for: Questionnaires: Where to send information (currently an SDC extension with just the address) ValueSet: Where related Terminology Services can be found (where not local) Subscription: The destination to send the subscribed data (or to pull) Messaging: (currently defined in the Message Header, but only as the address) Referrals: Where to send referral requests (linked to the services directory resources - Organization/Location/Practitioner/HealthcareService) Referrals - Templates: Where to locate referral templates (Questionnaires) (linked to the services directory resources - Organization/Location/Practitioner/HealthcareService) CarePlans: Where a shared CarePlan can be found Scheduling: Where to lookup to discover schedules/availability information (linked to the services directory resources - Organization/Location/Practitioner/HealthcareService) Scheduling: Where to lookup to send appointment requests (linked to the services directory resources - Organization/Location/Practitioner/HealthcareService) Patient/Person: Location of Master Patient/Person Indexes Service Provider Directories: Location of related directories (parent/child/federated) DICOM/imaging: Location of where to query for imaging content and metadata (QIDO-RS, WADO-RS, or WADO-URI) **Relationships** CapabilityStatement The endpoint is distinct from a capability statement in that the CapabilityStatement statement describes the entire capability of a server (and in the metadata case, just this server) Where the endpoint resource describes the technical details for how to connect, and for what purposes (which could be a small sub-set of the server's capabilities, and might not be a FHIR endpoint). ContactPoint Endpoints are used to facilitate system to system communication, providing the details of the addressing of the system, purpose of use, protocols required/provided, and any other details required to communicate between the systems. (such as configuration parameters, require headers ...) The address value in the endpoint can only be used in the context of the provided details. E.g. If the endpoint has a URL for an XDS service, using that in a browser by a user is meaningless. E.g.2 If the endpoint has a US Direct address, using this to send a normal email message will not work, as they are not true email addresses (even though they appear to be) A ContactPoint contains contact information, such as a phone number, web address that a user can directly interact with, no additional information would be required to use the value. The value is generally used to communicate with an entity directly, not a system. **Background and Context** Expected Implementations Any solution where there are distributed FHIR servers deployed and need discovery/configuration (Refer to the scope and usage section for common uses) **Referenced By** HealthcareService ImagingStudy InsurancePlan Location Organization OrganizationAffiliation PractitionerRole --- ## EnrollmentRequest import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/enrollmentrequest.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; This resource provides the insurance enrollment details to the insurer regarding a specified coverage. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource has not yet undergone proper review by FM. At this time it is a 'stub', is known to be incomplete, and is to be considered as a draft. The EnrollmentRequest resource allows for the addition and removal of plan subscribers and their dependents to health insurance coverage. Todo **Referenced By** EnrollmentResponse --- ## EnrollmentResponse import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/enrollmentresponse.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; This resource provides enrollment and plan details from the processing of an EnrollmentRequest resource. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource has not yet undergone proper review by FM. At this time it is a 'stub', is known to be incomplete, and is to be considered as a draft. The EnrollmentResponse resource provides enrollment and plan details from the processing of an Enrollment resource. --- ## EpisodeOfCare import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/episodeofcare.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; An association between a patient and an organization / healthcare provider(s) during which time encounters may occur. The managing organization assumes a level of responsibility for the patient during this time. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The EpisodeOfCare Resource contains information about an association of a Patient with a Healthcare Provider for a period of time under which related healthcare activities may occur. In many cases, this represents a period of time where the Healthcare Provider has some level of responsibility for the care of the patient regarding a specific condition or problem, even if not currently participating in an encounter. These resources are typically known in existing systems as: EpisodeOfCare: Case, Program, Problem, Episode Encounter: Visit, Contact Multiple Organizations and Transfer of Care Many organizations can be involved in an EpisodeOfCare; however each organization will have its own EpisodeOfCare resource instance that tracks its responsibility with the patient. When an Organization completes their involvement with the patient and transfers care to another Organization. This is often in the form of a referral to another Organization (or Organizations). When an incoming referral is received a new EpisodeOfCare may be created for this organization. The initial step(s) in the intake workflow for the referral often involve some form of assessment(s), eligibility, capacity, care levels, which could take some time. Once the intake process is completed and the patient is accepted, a CarePlan is often created. **Relationships** The primary difference between the EpisodeOfCare and the Encounter is that the Encounter records the details of an activity directly relating to the patient, while the EpisodeOfCare is the container that can link a series of Encounters together for problems/issues. The Example scenarios below give some good examples as to when you might want to be using an EpisodeOfCare. This difference is a similar difference between the EpisodeOfCare and a CarePlan. The EpisodeOfCare is a tracking resource, rather than a planning resource. The EpisodeOfCare usually exists before the CarePlan. You don't need a CarePlan to use an EpisodeOfCare. **Background and Context** Systems collect a coherent group of activities (such as encounters) related to a patient's health condition or problem often referred to as a Care Episode. Information about an episode is often shared across systems, and in some cases organizational and disciplinary boundaries. An EpisodeOfCare contains details about the purpose of the care and can exist without any activities. The minimal information that would be required in an episode of care would be a patient, organization and a reason for the ongoing association. Other reasons for creating an EpisodeOfCare could be for tracking the details required for government reporting or billing. Expected Implementations Chronic Disease Management Systems Community Care Systems Tracking progress of a specific condition Tracking government funding Problem based General Practice systems Disability Support Systems Aged Care Systems (Community and Residential) **Referenced By** ChargeItem Contract DocumentReference Encounter MedicationAdministration MedicationDispense MedicationStatement --- ## EventDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/eventdefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The EventDefinition resource provides a reusable description of when a particular event can occur. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is a definition resource from a FHIR workflow perspective - see Workflow, specifically Definition. The EventDefinition provides a reusable description of an event. The resource supports describing different kinds of events, including named events, periodic events, and data-based events. For each of these, the resource also supports a formal description of the event. For example, a 'monitor-emergency-admissions' event can be a named event, but also provide a formal description of the event as monitoring for encounters that occur in emergency department locations. **Relationships** The EventDefinition resource is intended to be used to provide a reusable description of events for use by other resources that require event definition, such as Subscription. EventDefinition is a definition of an event that could occur, whereas Subscription is asking for notification of events when they occur. --- ## Evidence import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/evidence.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The Evidence resource describes the conditional state (population and any exposures being compared within the population) and outcome (if specified) that the knowledge (evidence, assertion, recommendation) is about. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The scope of the Evidence resource is to describe the conditional state (population and exposures being considered or compared within a population) with or without the outcomes that knowledge (evidence, assertion, recommendation) is about. The Evidence resource is a commonly bundled series of evidence variables from the "PICO" specification of medical knowledge: Population - the population within which exposures and outcomes are being considered Intervention - the conditional state or exposure state being described for which one is expressing the risk of or effect on an outcome Comparison - the alternative conditional state or alternative exposure state when one is comparing states for the effect on an outcome Outcome - the result or effect of an intervention or exposure in a population PICO (and its variants like PECO or PICOT) is a universal acronym used in evidence-based medicine communities to clearly express research questions and evidence findings. The Evidence resource allows expression of the higher-order concept for recommendations in healthcare where multiple outcomes are considered, and the higher-order concept/combination for framing research questions. **Relationships** The Evidence resource references the EvidenceVariable resource two or more times to clearly specify the relationship among evidence variable concepts in framing a research question, evidence assertion, or recommendation. --- ## EvidenceVariable import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/evidencevariable.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The EvidenceVariable resource describes a "PICO" element that knowledge (evidence, assertion, recommendation) is about. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is a definition resource from a FHIR workflow perspective - see Workflow, specifically Definition. The scope of the EvidenceVariable resource is to describe a "PICO" element that knowledge (evidence, assertion, recommendation) is about. PICO stands for: Population - the population within which exposures and outcomes are being considered Intervention - the conditional state or exposure state being described for which one is expressing the risk of or effect on an outcome Comparison - the alternative conditional state or alternative exposure state when one is comparing states for the effect on an outcome Outcome - the result or effect of an intervention or exposure in a population PICO (and its variants like PECO or PICOT) is a universal acronym used in evidence-based medicine communities to clearly express research questions and evidence findings. The EvidenceVariable resource allows expression of the components of a PICO question in codeable and reusable formats. **Relationships** Multiple resources describing evidence findings (Evidence, EffectEvidenceSynthesis, EffectEvidenceBit, RiskEvidenceSynthesis, RiskEvidenceBit) will reference EvidenceVariable to describe what the evidence findings are about. **Referenced By** EffectEvidenceSynthesis Evidence RiskEvidenceSynthesis --- ## ExampleScenario import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/examplescenario.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Example of workflow instance. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Todo --- ## ExplanationOfBenefit import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/explanationofbenefit.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; This resource provides: the claim details; adjudication details from the processing of a Claim; and optionally account balance information, for informing the subscriber of the benefits provided. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The ExplanationOfBenefit (EOB) resource combines key information from a Claim, a ClaimResponse and optional Account information to inform a patient of the goods and services rendered by a provider and the settlement made under the patient's coverage in respect of that Claim. The ExplanationOfBenefit resource may also be used as a resource for data exchange for bulk data analysis, as the resource encompasses Claim, ClaimResponse and Coverage/Eligibility information. This is the logical combination of the Claim, ClaimResponse and some Coverage accounting information in respect of a single payor prepared for consumption by the subscriber and/or patient. It is not simply a series of pointers to referred-to content models, is a physical subset scoped to the adjudication by a single payor which details the services rendered, the amounts to be settled and to whom, and optionally the coverage allowed under the policy and the amounts used to date. Typically the EOB is only used to convey Claim (use=claim) and the associated ClaimResponse information to patients or subscribers. It may also be used to convey consolidated predetermination and preauthorization request and response information to patients or subscribers. An EOB will never be created for patient or subscriber information exchange if an error was detected in the Claim. It is also recognized that "EOB" is a term that carries additional meaning in certain areas of the industry. When the resource was originally being developed there was substantial discussion about the adoption of an alternative name for the resource but after much discussion it was resolved that the ExplanationOfBenefit name has the advantage of familiarity that has been proven through the early adoption of the resource for multiple purposes. Note: when creating profiles for EOB as a patient focused information exchange the payment details, other than date, should be excluded if the payee is the provider as that would leak business confidential information. Note: the EOB SHALL NOT be used as a replacement for a ClaimResponse when responding to Claims. Only the ClaimResponse contains the appropriate adjudication information for a payor response to a Claim. The ExplanationOfBenefit resource is an "event" resource from a FHIR workflow perspective - see Workflow Event. Additional Information Additional information regarding electronic claims content and usage may be found at: Financial Resource Status Lifecycle: how .status is used in the financial resources. Secondary Use of Resources: how resources such as Claim and ExplanationOfBenefit may used for reporting and data exchange for analytics, not just for eClaims exchange between providers and payors. Subrogation: how eClaims may handle patient insurance coverages when another insurer rather than the provider will settle the claim and potentially recover costs against specified coverages. Coordination of Benefit: how eClaims may handle multiple patient insurance coverages. RealTime Exchange and Obtaining EOBs: EOBs may be obtained using Polling or FHIR REST (SEARCH). Attachments and Supporting Information: how eClaims may handle the provision of supporting information, whether provided by content or reference, within the eClaim resource when submitted to the payor or later in a resource which refers to the subject eClaim resource. This also includes how payors may request additional supporting information from providers. **Relationships** The ExplanationOfBenefit resource is for reporting out to patients or transferring data to patient centered applications, such as patient health Record (PHR) application, the ExplanationOfBenefit should be used instead of the Claim and ClaimResponse resources as those resources may contain provider and payer specific information which is not appropriate for sharing with the patient. When using the resources for reporting and transferring claims data, which may have originated in some standard other than FHIR, the Claim resource is useful if only the request side of the information exchange is of interest. If, however, both the request and the adjudication information is to be reported then the ExplanationOfBenefit should be used instead. The Claim resource is used to request the adjudication and/or authorization of a set of healthcare-related goods and services for a patient against the patient's insurance coverages, or to request what the adjudication would be for a supplied set of goods or services should they be actually supplied to the patient. When requesting whether the patient's coverage is inforce, whether it is valid at this or a specified date, or requesting the benefit details or preauthorization requirements associated with a coverage CoverageEligibilityRequest should be used instead. The eClaim domain includes a number of related resources ExplanationOfBenefit | This resource combines the information from the Claim and the ClaimResponse, stripping out any provider or payor proprietary information, into a unified information model suitable for use for: patient reporting; transferring information to a Patient Health Record system; and, supporting complete claim and adjudication information exchange with regulatory and analytics organizations and other parts of the provider's organization. Claim | A suite of goods and services and insurances coverages under which adjudication or authorization is requested. ClaimResponse | A payor's adjudication and/or authorization response to the suite of services provided in a Claim. Typically the ClaimResponse references the Claim but does not duplicate the clinical or financial information provided in the claim. CoverageEligibilityRequest | A request to a payor to: ascertain whether a coverage is in-force at the current or at a specified time; list the table of benefits; determine whether coverage is provided for specified categories or specific services; and whether preauthorization is required, and if so what supporting information would be required. --- ## FamilyMemberHistory import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/familymemberhistory.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Significant health conditions for a person related to the patient relevant in the context of care for the patient. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** FamilyMemberHistory is one of the event resources in the FHIR workflow specification. This resource records significant health conditions for a particular individual related to the subject. This information can be known to different levels of accuracy. Sometimes the exact condition ('asthma') is known, and sometimes it is less precise ('some sort of cancer'). Equally, sometimes the person can be identified ('my aunt Agatha') and sometimes all that is known is that the person was an uncle. This resource represents a simple structure used to capture an 'elementary' family history for a particular family member. However, it can also be the basis for capturing a more rigorous history useful for genetic and other analysis - refer to the Genetic Pedigree profile for an example. The entire family history for an individual can be represented by combining references to FamilyMemberHistory instances into a List resource instance. **Referenced By** AdverseEvent ClinicalImpression --- ## Flag import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/flag.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Prospective warnings of potential issues when providing care to the patient. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A flag is a warning or notification of some sort presented to the user - who may be a clinician or some other person involve in patient care. It usually represents something of sufficient significance to warrant a special display of some sort - rather than just a note in the resource. A flag has a subject representing the resource that will trigger its display. This subject can be of different types, as described in the examples below: A note that a patient has an overdue account, which the provider may wish to discuss with them - in case of hardship for example (subject = Patient) An outbreak of Ebola in a particular region (subject=Location) so that all patients from that region have a higher risk of having that condition A particular provider is unavailable for referrals over a given period (subject = Practitioner) A patient who is enrolled in a clinical trial (subject=Group) Special guidance or caveats to be aware of when following a protocol (subject=PlanDefinition) Warnings about using a drug in a formulary requires special approval (subject=Medication) etc. A flag is typically presented as a label in a prominent location in the record to notify the clinician of the potential issues, though it may also appear in other contexts; e.g. notes applicable to a radiology technician, or to a clinician performing a home visit. For patients, the information in the flag will often be derived from the record, and therefore, for a thorough and careful clinician, who has the time to review the notes will be redundant. However, given the volume of information frequently found in patients' records and the potentially serious consequences of losing sight of some facts, this redundancy is deemed appropriate. As well, some flags may reflect information not captured by any other resource in the record. (E.g. "Patient has large dog at home") In line with its purpose, a flag is concise, highlighting a small set of high-priority issues among the much larger set of data in the chart. Readers who want more detail should consult the chart or other source of information. Caution should be exercised in creating Flag instances. If entries are created for information that could be gleaned in a sufficiently timely fashion by reviewing the patient record, the flag list will itself become overwhelming and will cease to serve its intended purpose. Flags are expected to persist in a record for some period of time and are, at most, targeted to particular types of practitioners or to practitioners in particular system. Examples of Patient related issues that might appear in flags: Risks to the patient (functional risk of falls, spousal restraining order, latex allergy) Patient's needs for special accommodations (hard of hearing, need for easy-open caps) Risks to providers (dog in house, patient may bite, infection control precautions) Administrative concerns (incomplete information, pre-payment required due to credit risk) Examples of issues that should not appear only in flags: Potential allergy or drug interaction to planned therapy (use DetectedIssue) Known adverse reaction to a substance (use AllergyIntolerance) Note that we include "latex allergy" in the "in scope" list, and "allergy" in the "not in scope" list. The Flag resource is not designed to replace the normal order checking process, and one should not expect to see all allergies in Flags. However, if there is an activity that might occur prior to careful evaluation of the record (e.g. donning of latex gloves) and that activity might pose a risk to the patient, that is the sort of eventuality the Flag is intended to support. Specific guidelines about what type of information is appropriate to expose using Flag, as well as what categories of individuals should see particular flags, will vary by interoperability community. **Relationships** Flags may highlight a highly condensed view of information found in the AllergyIntolerance, Condition, Observation, Procedure and possibly other resources. A common extension allows the linkage of a Flag to the supporting detail resource. The purpose of these other resources is to provide detailed clinical information. The purpose of a Flag is to alert practitioners to information that is important to influence their interaction with a Patient prior to detailed review of the record. Flags are not used to convey information to a specific individual or organization (e.g. an abnormal lab result reported to the ordering clinician, reporting of an adverse reaction to a regulatory authority). These are handled using the CommunicationRequest and the Communication resources. Flags are not raised as a result of a reported or proposed action (e.g. drug-drug interactions, duplicate therapy warnings). These would be handled using DetectedIssue. --- ## Goal import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/goal.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Describes the intended objective(s) for a patient, group or organization care, for example, weight loss, restoring an activity of daily living, obtaining herd immunity via immunization, meeting a process improvement objective, etc. Refer to the [US Core Goal profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-goal.html). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A Goal in health care services delivery is generally an expressed desired health state to be achieved by a subject of care (or family/group) over a period or at a specific point of time. This desired target health state may be achieved as a result of health care intervention(s) or resulting from natural recovery over time. For example: A goal of a plan for a condition such as a diabetes might specify desired outcome(s) (e.g. HgbA1c level =<5.6% in 3 months) as a result of interventions such as medication therapy, nutritional management and/or increase physical activity. A goal of a procedure might be to meet the intended objective of the procedure (e.g. wet-dry-dressing changes twice a day; goal: wound healed completely in 2 weeks) or to prevent an unintended complication (e.g. repositioning a patient every two hours: goal to maintain skin integrity) Goals may address the prevention of illness, cure or mitigation of a condition, prolongation of life, or mitigation of pain and discomfort. When dealing with groups, goals may also reflect health state, such as a reduction of addiction behaviors. However, they may also reflect population health objectives such as education, screening, etc. Organizational goals are typically not health state specific but may instead identify measurement targets such as infection control, cost management, patient satisfaction, etc. **Relationships** Goals are typically established in the context of a CarePlan. However, goals may also be directly referenced by request-type resources (e.g. MedicationRequest or ServiceRequest) by using an extension. A goal represents a specific goal instance for a particular patient, group, etc. It is not intended to be used to define types of potential goals as part of an order set or protocol definition. Protocol definitions and order sets are supported through PlanDefinition. The Goal resource is intended to be used once an order set is instantiated or assigned to a patient, which is when the potential goals become the actual goals, if not changed or deleted. Goals are often evaluated using Observations. **Referenced By** CarePlan --- ## GraphDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/graphdefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A formal computable definition of a graph of resources - that is, a coherent set of resources that form a graph by following references. The Graph Definition resource defines a set and makes rules about the set. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The GraphDefinition resource provides a formal computable definition of a graph of resources - that is, a coherent set of resources that form a graph by following references. The Graph Definition resource defines a set and makes rules about the set. The GraphDefinition resource can be used to: Summarize a set of profiles on resources Define a graph of resources to return in a query Define a graph of resources to include in a document Document rules about the relationship between a set of resources e.g. must all resources concern the same patient? **Relationships** There is a close relationship between Profiles and GraphDefinitions: A StructureDefinition defines a profile, and profiles can make rules about the relationships between resources. A carefully defined set of profiles implies part of what is in a GraphDefinition A GraphDefinition defines rules about the relationships between resources, and in so doing, implies some constraints that would need to be represented in their profiles Profiles and Graph Definitions can be used together, or separately. When used together, they should be consistent. Note, though, that a graph definition may contain a subset or a superset of the relationships explicitly described in the profiles it refers to. It is possible that in some circumstances, a graph definition makes incompatible rules with the Profiles it refers to - in this case, no graph if resources will meet the constraints expressed. Applications should - but are not required - detect when such incompatibilities arise. **Referenced By** MessageDefinition --- ## Group import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/group.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Represents a defined collection of entities that may be discussed or acted upon collectively but which are not expected to act collectively, and are not formally or legally recognized; i.e. a collection of entities that isn't an Organization. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Use Cases The Group resource is used in one of two ways: To define a group of specific people, animals, devices, etc. that is being tracked, examined or otherwise referenced as part of healthcare-related activities To define a set of possible people, animals, devices, etc. that are of interest for some intended future healthcare-related activities Examples of the former could include group therapy or treatment sessions, exposed entities tracked as part of public health, etc. The latter might be used to define expected subjects for a clinical study. Both use cases are handled by a single resource because the data elements captured tend to be similar. **Relationships** There are a number of mechanisms in FHIR for communicating collections of resources: The List resource - enumerates a flat collection of resources and provides features for managing the collection. While a particular List instance may represent a "snapshot", from a business process perspective the notion of "List" is dynamic – items are added and removed over time. The List resource references other resources. Lists may be curated and have specific business meaning. This Group resource - defines a group of specific people, animals, devices etc. by enumerating them, or by describing qualities that group members have. The group resource refers to other resources, possibly implicitly. Groups are intended to be acted upon or observed as a whole; e.g. performing therapy on a group, calculating risk for a group, etc. This resource will commonly be used for public health (e.g. describing an at-risk population), clinical trials (e.g. defining a test subject pool) and similar purposes. CareTeam. Group is distinct from CareTeam. Group is patient-independent and identifies an undifferentiated set of individuals who are intended to be the target of one or more clinical activities (e.g. set of clinical trial participants, set of individuals impacted by or at risk of a public health event, a herd or flock, etc.) The CareTeam resource establishes a set of relationships and roles and is specific to a particular Patient. The actors are the individual members or organized group of individuals. CareTeam can be referenced by EpisodeOfCare, Encounter, or CarePlan to identify the set of individuals (and their respective roles) who are intended to be involved in providing the care defined by those resources. The Bundle resource - is an infrastructure container for a group of resources. It does not have narrative and is used to group collections of resources for transmission, persistence or processing (e.g. messages, documents, transactions, query responses, etc.) The content of bundles is typically algorithmically determined for a particular exchange or persistence purpose. The Composition resource - defines a set of healthcare-related information that is assembled together into a single logical document that provides a single coherent statement of meaning, establishes its own context and that has clinical attestation with regard to who is making the statement. The composition resource provides the basic structure of a FHIR document. The full content of the document is expressed using a bundle. Compositions will often reference Lists as the focus of particular sections. The DomainResource.contained element - allows multiple resources to be nested inside any DomainResource. This is a special type of grouping where the grouped resources lose independent existence - they no longer have their own identifiers, can't easily be queried independently, etc. Use of this grouping is a technical mechanism for managing the independence of resources and has no impact on meaning. Contained, bundled and remotely referenced resources convey the same meaning. **Referenced By** DataRequirement UsageContext ActivityDefinition AdverseEvent CarePlan CareTeam ChargeItem ClinicalImpression Communication CommunicationRequest Condition Consent Contract DeviceRequest DeviceUseStatement DiagnosticReport DocumentManifest DocumentReference Encounter EventDefinition EvidenceVariable Flag Goal GuidanceResponse ImagingStudy Invoice Library List Measure MeasureReport Media MedicationAdministration MedicationDispense MedicationRequest MedicationStatement Observation PlanDefinition Procedure RequestGroup ResearchDefinition ResearchElementDefinition ResearchStudy RiskAssessment ServiceRequest Specimen Task --- ## GuidanceResponse import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/guidanceresponse.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A guidance response is the formal response to a guidance request, including any output parameters returned by the evaluation, as well as the description of any proposed actions to be taken. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The GuidanceResponse resource is used to represent the result of invoking a decision support service. It provides a container for the status of the response, any warnings or messages returned by the service, as well as any output data from the service and any suggested actions to be performed. Use cases for the GuidanceResponse resource include: Keeping a record of past service requests, including the ability to reference the context of the request such as the disease state Representing additional data requirements for subsequent service requests Representing Public Health case reporting and recording Sharing the results of decision support guidance and determination of reportability with clinicians and public health agencies In addition, the GuidanceResponse resource is aligned with the CDS Hooks response structure and so provides a mechanism to record the results of CDS Hooks interactions. For a detailed discussion of how the GuidanceResponse resource can be used for this purpose, refer to the Evaluation topic in the Clinical Reasoning module. --- ## HealthcareService import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/healthcareservice.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The details of a healthcare service available at a location. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The HealthcareService resource is used to describe a single healthcare service or category of services that are provided by an organization at a location. The location of the services could be virtual, as with telemedicine services. Common examples of HealthcareServices resources are: Allied Health Clinical Neuropsychologist Podiatry Service Smallville Hospital Emergency Services Respite care provided at a nursing home or hostel 24hr crisis telephone counseling service Information, advice and/or referral services; Disability, Telecommunications Rural TeleHealth Services Hospital in the home Yellow Cabs Pharmacy Active Rehab Social Support Drug and/or alcohol counseling Day Programs, Adult Training & Support Services Consulting psychologists and/or psychology services Group Hydrotherapy Little River Home Maintenance HealthcareService resources do not represent Computer related Services (not SOA) Example uses of HealthcareService resources are: National Services Directory - Consumer Focus National Services Directory - Practitioner Referrals Searching Organization's Client Portal - to locate services / book appointments Address book of services for Referrals including references to Questionnaires for assessments that are required as part of the referral Health Network internal directory Used for tracking available services offered internally, and also those offered by business partners. This information may also include costing information. **Relationships** The HealthcareService resource can be used with the Schedule resource to define actual availability of the service. This would be done by using the Schedule's Actor property. When creating an Appointment, the HealthcareService is to be assigned to one of the participants. It is up to the scheduling system to determine if the service is available and can be accepted. The HealthcareService resource is used with the following resources: Organization: The organization provides the services, the healthcareservice describes the services ServiceRequest: This is a subject specific request for a specific service, it may be to a specific healthcare service, and/or simply a coded service value. The granularity included in the request codes may be finer than defined in the healthcare service. OrganizationAffiliation: Might constrain a list of healthcare services that are available between 2 organizations Location: Specifies the place where the service(s) are offered/available within. **Referenced By** UsageContext Account Appointment AppointmentResponse CarePlan CatalogEntry Communication CommunicationRequest DeviceRequest OrganizationAffiliation PractitionerRole Schedule ServiceRequest SupplyRequest Task --- ## ImagingStudy import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/imagingstudy.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Representation of the content produced in a DICOM imaging study. A study comprises a set of series, each of which includes a set of Service-Object Pair Instances (SOP Instances - images or other data) acquired or produced in a common context. A series is of only one modality (e.g. X-ray, CT, MR, ultrasound), but a study may have multiple series of different modalities. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** ImagingStudy provides information on a DICOM imaging study, and the series and imaging objects in that study. It also provides information on how to retrieve that information (in a native DICOM format, or in a rendered format, such as JPEG). ImagingStudy is used to make available information about all parts of a single DICOM study. This resource provides mappings of its elements to DICOM attributes. DICOM attributes are identified by a 32-bit tag, presented in canonical form as two four-digit hexadecimal values within parentheses and separated by a comma, e.g. (0008,103E). The name and value representation (data type) of each attribute can be found in DICOM Part 6 Data Dictionary . The use of the attributes in the context of information objects, including detailed description of use, can be found in DICOM Part 3 Information Object Definitions . Attributes used in the DICOM query information models, such as "Number of Instances in Study", can be found in DICOM Part 4 Annex C . ImagingStudy provides access to significant DICOM information but will only eliminate the need for DICOM query (e.g., QIDO-RS) in the simplest cases. The DICOM instances are not stored in the ImagingStudy resource; use of a DICOM WADO-RS server or other storage mechanism is needed. An ImagingStudy SHALL reference one DICOM Study, and MAY reference a subset of that Study. More than one ImagingStudy MAY reference the same DICOM Study or different subsets of the same DICOM Study. **Relationships** ImagingStudy is used for DICOM imaging and associated information. Use Media to track non-DICOM images, video, or audio. Binary can be used to store arbitrary content. DocumentReference allow indexing and retrieval of clinical “documents” with relevant metadata. **Referenced By** ChargeItem ClinicalImpression DiagnosticReport Observation --- ## Immunization import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/immunization.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Describes the event of a patient being administered a vaccine or a record of an immunization as reported by a patient, a clinician or another party. Refer to the [US Core Immunizations profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-immunization.html). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The Immunization resource is intended to cover the recording of current and historical administration of vaccines to patients across all healthcare disciplines in all care settings and all regions. This includes immunization of both humans and animals but does not include the administration of non-vaccine agents, even those that may have or claim to have immunological effects. While the terms "immunization" and "vaccination" are not clinically identical, for the purposes of the FHIR resources, the terms are used synonymously. Additionally, the Immunization resource is expected to cover key concepts related to the creation, revision and querying of a patient's immunization history. This resource - through consultation with the Public Health work group - is believed to meet key use cases and information requirements as defined in the existing HL7 v2 immunization implementation guide, HL7 v3 POIZ domain and Immunization Domain Analysis Model. **Relationships** Administration of vaccines is intended to be handled using the Immunization resource. MedicationAdministration is intended for tracking the administration of non-vaccine medications. Some systems treat immunizations in the same way as any other medication administration. Such systems SHOULD use an immunization resource to represent these. If systems need to use a MedicationAdministration resource to capture vaccinations for workflow or other reasons, they SHOULD also expose an equivalent Immunization instance. Immunization.reaction may be an indication of an allergy or intolerance. If this is deemed to be the case, a separate AllergyIntolerance resource instance should be created to indicate it, as most systems will not query against past Immunization.reaction. **Referenced By** AdverseEvent ChargeItem ImmunizationEvaluation ImmunizationRecommendation Observation --- ## ImmunizationEvaluation import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/immunizationevaluation.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Describes a comparison of an immunization event against published recommendations to determine if the administration is "valid" in relation to those recommendations. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The ImmunizationEvaluation resource is intended to cover communicating the results of an evaluation of a vaccine administration event (documented using the Immunization resource) against a set of published recommendations (protocols). Evaluating administered doses in a patient's immunization history is central to ensuring that the patient if fully protected against vaccine-preventable diseases. It is also a necessary prerequisite to generating a customized set of immunization recommendations for the patient. **Relationships** The ImmunizationEvaluation resource communicates the results of an evaluation of an instance of the Immunization resource relative to a set of published recommendations. It is intended to communicate if the vaccine administration event was "valid" or "not valid" relative to the recommendations. Once all vaccine administration events in a patient's record have been evaluated, it is then possible to generate a set of vaccination related recommendations, unique for the patient. These recommendations are communicated using the ImmunizationRecommedation resource. As the ImmunizationEvaluation resource is focused on the evaluation of a single vaccine administration event, it does not include data regarding the overall patient status relative to the recommendations used in the evaluation. For example, a total of 3 doses of hepatitis B vaccine may be required for patient protection, but an instance of the ImmunizationEvaluation resource will be concerned with only one of the doses (although other instances of ImmunizationEvaluation should exist for the other doses). The ImmunizationRecommendation resource is used to convey the overall patient status (eg. a dose is due or the patient is complete) relative to the hepatitis B series. **Background and Context** Through FHIR Release 3, the Immunization resource contained a "protocol" backbone element which functioned as an evaluation of the vaccine administration event. Beginning in Release 4, this new resource replaces the Immunization.protocol element. **Referenced By** ImmunizationRecommendation --- ## ImmunizationRecommendation import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/immunizationrecommendation.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A patient's point-in-time set of recommendations (i.e. forecasting) according to a published schedule with optional supporting justification. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The ImmunizationRecommendation resource is intended to cover communication of a specified patient's immunization recommendations and status across all healthcare disciplines in all care settings and all regions. The set of customized (for the patient) recommendations is based on the comparison of the patient's immunization history with a set of published recommendations (protocols). Additionally, the ImmunizationRecommendation resource is expected to cover key concepts related to the querying of a patient's immunization recommendations and status. This resource - through consultation with the Public Health work group - is believed to meet key use cases and information requirements as defined in the existing HL7 v3 POIZ domain and Immunization Domain Analysis Model. **Relationships** This resource references the following resources: Patient Organization Immunization ImmunizationEvaluation **Referenced By** Appointment DiagnosticReport Encounter MedicationRequest Observation --- ## ImplementationGuide import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/implementationguide.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A set of rules of how a particular interoperability or standards problem is solved - typically through the use of FHIR resources. This resource is used to gather all the parts of an implementation guide into a logical whole and to publish a computable definition of all the parts. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** An implementation guide (IG) is a set of rules about how FHIR resources are used (or should be used) to solve a particular problem, with associated documentation to support and clarify the usage. Classically, FHIR implementation guides are published on the web after they are generated using the FHIR Implementation Guide Publisher . The ImplementationGuide resource is a single resource that defines the logical content of the IG, along with the important entry pages into the publication, so that the logical package that the IG represents, so that the contents are computable. In particular, validators are able to use the ImplementationGuide resource to validate content against the implementation guide as a whole. The significant conformance expectation introduced by the ImplementationGuide resource is the idea of Default Profiles. Implementations may conform to multiple implementation guides at once, but this requires that the implementation guides are compatible (see below). **Relationships** Implementation Guides contain two different types of resource references: Contents: A set of logical statements which implementations must conform to. These are almost always conformance resources Examples: Examples that illustrate the intent of the profiles defined in the implementation guide. These can be any kind of resource An application's Capability Statement may identify one or more implementation guides that an application conforms to. **Referenced By** ElementDefinition CapabilityStatement --- ## InsurancePlan import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/insuranceplan.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Details of a Health Insurance product/plan provided by an organization. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A product is a discrete package of health insurance coverage benefits that are offered under a particular network type. A given payer’s products typically differ by network type and/or coverage benefits. A plan pairs the health insurance coverage benefits under a product with the particular cost sharing structure offered to a consumer. A given product may comprise multiple plans. InsurancePlan describes a health insurance offering comprised of a list of covered benefits (i.e. the product), costs associated with those benefits (i.e. the plan), and additional information about the offering, such as who it is owned and administered by, a coverage area, contact information, etc. **Relationships** TBD. **Referenced By** UsageContext --- ## Invoice import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/invoice.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Invoice containing collected ChargeItems from an Account with calculated individual and total price for Billing purpose. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Tracking Financial information is vital in Patient Administration and Finance systems in most Healthcare Organizations. An invoice is a financial document issued by a healthcare provider to a patient or a payer indicating the goods and services (ChargeItems) performed with their quantities and prices. Example implementations: Any solution that tracks billing information and needs to issue invoices Private Insurance Providers who want to deliver structured information to patients to increase cost transparency Patient apps that want to include information on the amount and reason of the charged costs for the healthcare services a patient received **Relationships** The existing Claim resource is constricted to use cases where Claims are sent to insurances for reimbursement, in a message-like style. It neither references ChargeItems nor Accounts. Competing invoicing standards such as EDIFACT or X12 are aiming at inter-organizational exchange and do not offer human readable representations or traceable links to services rendered which is a requirement for invoicing towards patients. This resource is used where existing ChargeItems in an Account are collected to create an Invoice to be sent out to Individuals or Organizations in a structured way. The invoice is the hospital's view, whereas the Claim is the payer's view on the performed services. Since the individual ChargeItem resources that make up the line items of the Invoice do not carry any pricing information, ChargeItemDefinitions will have to be applied to the ChargeItems in order to calculate the price components for the Invoice depending on the context of the ChargeItem --- ## Library import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/library.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The Library resource is a general-purpose container for knowledge asset definitions. It can be used to describe and expose existing knowledge assets such as logic libraries and information model descriptions, as well as to describe a collection of knowledge assets. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The Library resource is a general purpose container for clinical knowledge assets. These assets may be defined using a non-FHIR representation, such as a shareable library of clinical logic, written in Clinical Quality Language (CQL), or the XML Schema for an information model, or they may be defined using FHIR resources such as the description of a protocol represented by a PlanDefinition resource. In addition to representing the metadata of the library, the resource has elements for tracking dependencies, as well as for representing the parameters and data requirements for any expression functionality provided by the library. For non-FHIR assets, the actual content of the library is represented using the Attachment data type, and may either be referenced with a URL, or the content may be embedded as a base-64 encoded string. Either way, the contentType element of the attachment is used to indicate the representation of the library content. Note that because the library content may be embedded as well as be retrievable from an external repository via the attachment URL, the possibility exists for the embedded content to be different from the content on the repository. With proper versioning and governance, this should never occur, but to minimize the potential impact of this possibility, implementers SHALL give precedence to the embedded content of a library when it is present. **Relationships** The Library resource is definitional in nature, meaning it is intended to represent shareable knowledge independent of any particular patient. This is in contrast to the DocumentReference resource, which captures non-FHIR content related to a specific patient, and the Media and Binary resources which capture multimedia content and raw binary content, respectively. **Referenced By** ActivityDefinition Measure PlanDefinition ResearchDefinition ResearchElementDefinition --- ## Linkage import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/linkage.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Identifies two or more records (resource instances) that refer to the same real-world "occurrence". **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource allows the assertion of linkages between multiple resource instances (generally of the same type) that are referring to the same underlying business objects. For example, multiple Condition records that refer to the same underlying problem/issue for a Patient; multiple AllergyIntolerance records that refer to the same reaction susceptibility; multiple Patient, Practitioner and/or RelatedPerson records that refer to the same human being or animal. FHIR supports a process for sharing electronic records. It is common for multiple records to exist that deal with the same real-world phenomenon. This can result from information being captured by different systems, information being captured within a single system by different users (either deliberately to represent distinct perspectives or accidentally when a new record is created rather than updating an existing record). These multiple records may be referred to as "duplicate" records, but in practice they aren't often actually "duplicate" in that the data represented (and the history of the records) will be at least somewhat distinct. In some cases, the solution after identifying duplicates is to deprecate one of the records (e.g. by changing the status to "Entered in Error") and to move all relevant information to the surviving record. In other cases, the resource may support the ability to establish a linkage directly between the resources. However, in some cases, both records may need to survive or there might be a desire to have both resources continue to be maintained, perhaps because the resources live on different servers or have different 'owners'. The Linkage resource is intended to satisfy this use-case. **Relationships** The Person resource should be used to link a person independent of a specific health-related context. Linkage can be used for all other use cases. --- ## List import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/list.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A list is a curated collection of resources. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The List resource is a flat, possibly ordered collection of records. List resources are used in many places, including allergies, medications, alerts, family history, medical history, etc. List resources can be used to support patient-specific clinical lists as well as lists that manage workflows such as tracking patients, managing teaching cases, etc. Resources supported by the List resource can be homogeneous – consisting of only one type of resource (e.g. allergy lists) as well as heterogeneous – containing a variety of resources (e.g. a problem list including Conditions, AllergyIntolerances, recent Procedures, etc.). Lists will typically include references to the resources that make up the list, however in some cases the details of the content of the list might be expressed in narrative only; e.g. a text record of a family history. The List resource is only needed if there is a need to filter the set of resources by a mechanism that cannot be accomplished via a simple query; e.g. there is no need to have a list for all AllergyIntolerances that exist on a server for a given patient. However, List is an appropriate mechanism to provide a filtered list of the subset of AllergyIntolerances that are deemed to be "current". Lists are allowed to contain other Lists, to create a nested collection of Lists. Querying a List of resources such as AllergyIntolerance, Condition or Medication-related resources is different than querying the resource-specific endpoint. For example, a List of AllergyIntolerance resources would represent a curated point-in-time snapshot of the patient's allergies and intolerances. On the other hand, querying the AllergyIntolerance endpoint would typically produce a larger set of records as it would both be non-curated (potentially containing duplicate or out-of-date records) and current - generated based on information as of "now" rather than the last time a human manually revised the List resource instance. Which mechanism is most appropriate for data retrieval will vary by use-case. In some cases, systems might not have an appropriate curated List to query. Note that the presence of an item in a List resource SHALL NOT change the meaning of any information that would be understood by looking at the item outside the context of the List, because items may be accessed directly outside the List by RESTful means or after a document is processed. For example, a List with a code that means "refuted conditions" cannot have items that are Condition resources that do not have a verificationStatus of refuted. **Relationships** There are five mechanisms in FHIR for communicating collections of resources: This List resource - enumerates a flat collection of resources and provides features for managing the collection. While a particular List instance may represent a "snapshot", from a business process perspective the notion of "List" is dynamic – items are added and removed over time. The List resource references other resources. Lists may be curated and have specific business meaning. The Group resource - defines a group of specific people, animals, devices, etc. by enumerating them, or by describing qualities that group members have. The group resource refers to other resources, possibly implicitly. Groups are intended to be acted upon or observed as a whole; e.g. performing therapy on a group, calculating risk for a group, etc. This resource will commonly be used for public health (e.g. describing an at-risk population), clinical trials (e.g. defining a test subject pool) and similar purposes. The Composition resource - defines a set of healthcare-related information that is assembled together into a single logical document that provides a single coherent statement of meaning, establishes its own context and that has clinical attestation with regard to who is making the statement. The Composition resource provides the basic structure of a FHIR document. The full content of the document is expressed using a bundle. Compositions will often reference Lists as the focus of particular sections. The Bundle resource - is an infrastructure container for a group of resources. It does not have a narrative and is used to group collections of resources for transmission, persistence or processing (e.g. messages, documents, transactions, query responses, etc.) The content of bundles is typically algorithmically determined for a particular exchange or persistence purpose. The DomainResource.contained element - allows multiple resources to be nested inside any DomainResource. This is a special type of grouping where the grouped resources lose independent existence - they no longer have their own identifiers, can't easily be queried independently, etc. Use of this grouping is a technical mechanism for managing the independence of resources and has no impact on meaning. Contained, bundled, and remotely referenced resources convey the same meaning. **Referenced By** MeasureReport --- ## Location import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/location.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Details and position information for a physical place where services are provided and resources and participants may be stored, found, contained, or accommodated. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A Location includes both incidental locations (a place which is used for healthcare without prior designation or authorization) and dedicated, formally appointed locations. Locations may be private, public, mobile or fixed and scale from small freezers to full hospital buildings or parking garages. Examples of Locations are: Building, ward, corridor, room or bed Mobile Clinic Freezer, incubator Vehicle or lift Home, shed, or a garage Road, parking place, a park Ambulance (generic) Ambulance (specific) Patient's Home (generic) Jurisdiction These locations are not intended to cover locations on a patient where something occurred (i.e. a patient's broken leg), but can happily cover the location where the patient broke the leg (the playground) **Relationships** Locations and Organizations are very closely related resources and can often be mixed/matched/confused. The Location is intended to describe the more physical structures managed/operated by an organization, whereas the Organization is intended to represent the more conceptual hierarchies, such as a ward. A Location is valid without an address in cases where it could be purely described by a geo-coded location in remote areas, or when recorded by a device. Locations with a mode = "kind" would also likely not have an address, as they are just a type of location, but could also have an address where they can be found at the address. Another use of location could be for describing a Jurisdiction. This jurisdiction may be considered a classified boundary which could be a combination of a physical boundary, and some other discriminator(s): Nation - Country-wide community or Federal Government (Ministry of Health) Province or State (community or Government) Business (throughout an enterprise) Business scope (CDC/FDA) Business segment (UK Pharmacy) **Referenced By** UsageContext Account ActivityDefinition AdverseEvent Appointment AppointmentResponse AuditEvent CarePlan Claim ClaimResponse Contract CoverageEligibilityRequest Device DeviceRequest DiagnosticReport Encounter ExplanationOfBenefit Flag HealthcareService ImagingStudy Immunization InsurancePlan List MeasureReport Media MedicationDispense Observation OrganizationAffiliation PractitionerRole Procedure Provenance ResearchStudy Schedule ServiceRequest Specimen SupplyDelivery SupplyRequest Task --- ## Measure import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/measure.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The Measure resource provides the definition of a quality measure. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The Measure resource represents a structured, computable definition of a health-related measure such as a clinical quality measure, public health indicator, or population analytics measure. A quality measure is a quantitative tool to assess the performance of an individual or organization with respect to a specified process or outcome via the measurement of actions, processes, or outcomes of clinical care. Quality measures are often derived from clinical guidelines and are designed to determine whether the appropriate care has been provided given a set of clinical criteria and an evidence base. Note that the Measure itself does not typically contain any logic; rather a Library resource is referenced that contains the logic required by the measure, and the various expression elements, such as population criteria, reference named expressions within that library (or libraries). In addition, if the Measure references multiple libraries, then any expression references within the resource must be qualified with the name of the library that contains the referenced expression. For a detailed discussion of how to use the Measure and MeasureReport resources, refer to the Quality Reporting topic. **Relationships** The Measure resource describes a specific quality measure, or population analytic, providing the structure of the measure in terms of the calculation elements (the populations involved). The Group resource is also capable of describing a population, however, the complexity involved in specifying the criteria in the general case requires the use of a high-level query language such as Clinical Quality Language (CQL). As such, the Measure resource defines only the top-level populations and references expressions for the actual criteria. These expressions are typically provided using a Library resource containing CQL or ELM expressions. In addition, the individual members of a population may be cases such as encounters or procedures and in these cases, the Group resource would be unable to represent the population characteristics accurately. A Measure is also similar to an Observation resource, with the exception that the Measure is purely definitional, it contains no actual measurements, only a description of how to calculate a particular measurement or set of measurements. A Measure is also similar to a clinical document, but as with the relationship to Observation, a Document is specific to a particular subject. **Referenced By** CarePlan Communication FamilyMemberHistory MeasureReport Procedure --- ## MeasureReport import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/measurereport.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The MeasureReport resource contains the results of the calculation of a measure; and optionally a reference to the resources involved in that calculation. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The MeasureReport resource represents the results of calculating a measure for a specific subject or group of subjects. The $evaluate-measure operation of the Measure resource is defined to return a MeasureReport. The resource is capable of representing three different levels of report: individual, subject-list, and summary. The resource draws requirements from the HL7 CDA R2 Implementation Guide: Quality Reporting Document Architecture - Category 1 (QRDA I) DSTU Release 3 (US Realm) and the HL7 Implementation Guide for CDA Release 2: Quality Reporting Document Architecture - Category III (QRDA III), DSTU Release 1 implementation guides. For a detailed discussion of how to use the Measure and MeasureReport resources, refer to the Quality Reporting topic. Note that this resource is a special case of the more general notion of a query evaluation result. However, because the general case requires the ability to represent arbitrary content, this resource uses a simple indicator structure to describe population sizes for each population type defined in the measure. The intent is to be able to represent the more general case as well, either by generalizing this resource, or by making this structure a profile of a more general resource, and we are actively seeking comments about what approaches might be taken to achieve that aim. **Relationships** Although the MeasureReport is conceptually an Observation, there is enough specific information required to support the quality reporting use case to warrant a separate resource. The resource is differentiated from a general purpose query result because it communicates specific information related to quality measurement evaluation that would be difficult to convey generally without imposing some other structure on top of the general results. --- ## Media import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/media.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A photo, video, or audio recording acquired or used in healthcare. The actual content may be inline or provided by direct reference. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage**
Trial-Use Note: The Orders and Observations workgroups wants to make the balloter aware than observations that require the attachment data types are represented by the Media resource instead of Observation. There have been several significant changes to the Media resource in order to align it with Observation and the workflow event pattern including: The addition of the partOf and issued elements. The required type was changed an optional category and the subtype element was renamed to modality. Additional reference targets added to the subject,operator, and basedOn elements The additions of several standard event extensions.
The Media resource contains photos, videos, and audio recordings. It is used with media acquired or used as part of the healthcare process. Here are some typical usages: Photos and videos of diagnostic or care provision procedures for recording purposes Images contained in diagnostic reports **Relationships** This resource captures a specific type of Observation - an Observation whose value is audio, video or image data. This resource is the preferred representation of such forms of information as it exposes the metadata relevant for interpreting the information. However, in some legacy environments, media information may occasionally appear in Observation instead. Systems should be aware of this possibility. The Media resource is able to contain medical images in a DICOM format. These images may also be made accessible through an ImagingStudy resource, which provides a direct reference to the image to a WADO-RS server. For such images, the WADO-RS framework is a preferred method for representing the images - the WADO-RS service may include rendering the image with annotations and display parameters from an associated DICOM presentation state, for instance. On the other hand, the media resource allows for a robust transfer of an image across boundaries where the WADO-RS service is not available. For this reason, medical images can also be represented in a Media resource, but the Media.content.url should provide a reference to a source WADO-RS service for the image. **Referenced By** AdverseEvent ClinicalImpression DeviceUseStatement DiagnosticReport ImagingStudy MedicationKnowledge Observation --- ## Medication import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/medication.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; This resource is primarily used for the identification and definition of a medication for the purposes of prescribing, dispensing, and administering a medication as well as for making statements about medication use. Refer to the [US Core Medication profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-medication.html). At Medplum, we encourage using [`MedicationKnowledge`](/docs/api/fhir/resources/medicationknowledge) rather than `Medication`. For more information, see [Modeling a Formulary](/docs/medications/formulary). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Representing medications in the majority of healthcare settings is a matter of identifying an item from a list and then conveying a reference for the item selected either into a patient-related resource or to other applications. Additional information about the medication is frequently provided for human verification, but a full representation of the details of composition and efficacy of the medicine is conveyed by referring to drug dictionaries by means of the codes they define. There are some occasions where it is necessary to identify slightly more detail, such as when dispensing a package containing a particular medication requires identification both of the medicine and the package at once. There are also some occasions (e.g. custom formulations) where the composition of a medicine must be represented. In these cases, the ingredients of the medicine have to be specified together with the amount contained, though the Medication resource does not provide full details. The Medication resource allows for medications to be characterized by the form of the drug and the ingredient (or ingredients), as well as how it is packaged. The medication will include the ingredient(s) and their strength(s) and the package can include the amount (for example, number of tablets, volume, etc.) that is contained in a particular container (for example, 100 capsules of Amoxicillin 500mg per bottle). The Medication resource can be used to describe a compounded (aka extemporaneous or magistral) product that is manufactured by the pharmacy at the time of dispensing. In this case there will be multiple ingredients which are typically base chemicals (for example, hydrocortisone powder) and there may be other ingredients that are manufactured products (for example, Glaxal Base). When a medication includes a package, further details about the composition can be provided. A package has a container (vacuum packed box, jar, etc.) and a list of the products or other packages that are in the package. **Referenced By** ActivityDefinition AdverseEvent CarePlan CatalogEntry ChargeItem ChargeItemDefinition Flag Group MedicationAdministration MedicationDispense MedicationKnowledge MedicationRequest MedicationStatement MedicinalProductContraindication MedicinalProductIndication MedicinalProductInteraction MedicinalProductUndesirableEffect Procedure SupplyDelivery SupplyRequest --- ## MedicationAdministration import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/medicationadministration.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Describes the event of a patient consuming or otherwise being administered a medication. This may be as simple as swallowing a tablet or it may be a long running infusion. Related resources tie this event to the authorizing prescription, and the specific encounter between patient and health care practitioner. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource covers the administration of all medications and vaccines. Please refer to the Immunization Resource/Profile for the treatment of vaccines. It will principally be used within care settings (including inpatient) to record the capture of medication administrations, including self-administrations of oral medications, injections, intra-venous adjustments, etc. It can also be used in outpatient settings to record allergy shots and other non-immunization administrations. In some cases, it might be used for home-health reporting, such as recording self-administered or even device-administered insulin. MedicationAdministration is an event resource from a FHIR workflow perspective - see Workflow Event **Relationships** The Medication domain includes a number of related resources MedicationRequest | An order for both supply of the medication and the instructions for administration of the medicine to a patient. MedicationDispense | Provision of a supply of a medication with the intention that it is subsequently consumed by a patient (usually in response to a prescription). MedicationAdministration | When a patient actually consumes a medicine, or it is otherwise administered to them MedicationStatement | This is a record of a medication being taken by a patient or that a medication has been given to a patient, where the record is the result of a report from the patient or another clinician. A medication statement is not a part of the prescribe->dispense->administer sequence but is a report that such a sequence (or at least a part of it) did take place, resulting in a belief that the patient has received a particular medication. MedicationAdministration is intended for tracking the administration of non-vaccine medications. Administration of vaccines is intended to be handled using the Immunization resource. Some systems treat immunizations in the same way as any other medication administration. Such systems SHOULD use an immunization resource to represent these. If systems need to use a MedicationAdministration resource to capture vaccinations for workflow or other reasons, they SHOULD also create and expose an equivalent Immunization instance. **Referenced By** AdverseEvent ChargeItem MedicationStatement Observation Procedure --- ## MedicationDispense import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/medicationdispense.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Indicates that a medication product is to be or has been dispensed for a named person/patient. This includes a description of the medication product (supply) provided and the instructions for administering the medication. The medication dispense is the result of a pharmacy system responding to a medication order. For Medplum best practices on using the `MedicationDispense` resource, please refer to [Representing Prescriptions and Medication Orders](/docs/medications/representing-prescriptions-and-medication-orders). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource covers the supply of medications to a patient. Examples include dispensing and pick-up from an outpatient or community pharmacy, dispensing patient-specific medications from inpatient pharmacy to ward, as well as issuing a single dose from ward stock to a patient for consumption. The medication dispense is the result of a pharmacy system responding to a medication order. MedicationDispense is an event resource from a FHIR workflow perspective - see Workflow Event **Relationships** The Medication domain includes a number of related resources MedicationRequest | An order for both supply of the medication and the instructions for administration of the medicine to a patient. MedicationDispense | Provision of a supply of a medication with the intention that it is subsequently consumed by a patient (usually in response to a prescription). MedicationAdministration | When a patient actually consumes a medicine, or the medication is otherwise administered to them MedicationStatement | This is a record of a medication being taken by a patient or that a medication has been given to a patient, where the record is the result of a report from the patient or another clinician. A medication statement is not a part of the prescribe->dispense->administer sequence but is a report that such a sequence (or at least a part of it) did take place, resulting in a belief that the patient has received a particular medication. This resource does not deal with the supply or transfer of non-medication related items to a patient. **Background and Context** The supply and the associated administration instructions might not exactly follow the original order (prescription), either because some details were left for completion at this point in the process or because the dispenser exercised their clinical judgment to make some appropriate modification. **Referenced By** ChargeItem MedicationStatement Observation --- ## MedicationKnowledge import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/medicationknowledge.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Information about a medication that is used to support knowledge. For Medplum best practices on using the `MedicationKnowledge` resource, please refer to [Modeling a Formulary](/docs/medications/formulary). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Note that this content is preliminary has not undergone proper review by the appropriate Workgroups. This resource supports use cases for creation of and querying for drug information including attributes such as drug classifications, images of medications, drug costs and/or coverages, etc. This resource can be used to return drug information as part of a formulary or a catalogue. --- ## MedicationRequest import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/medicationrequest.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; An order or request for both supply of the medication and the instructions for administration of the medication to a patient. The resource is called "MedicationRequest" rather than "MedicationPrescription" or "MedicationOrder" to generalize the use across inpatient and outpatient settings, including care plans, etc., and to harmonize with workflow patterns. Refer to the [US Core Medication Request profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-medicationrequest.html). For Medplum best practices on using the `MedicationRequest` resource, please refer to [Representing Prescriptions and Medication Orders](/docs/medications/representing-prescriptions-and-medication-orders). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource covers all type of orders for medications for a patient. This includes inpatient medication orders as well as community orders (whether filled by the prescriber or by a pharmacy). It also includes orders for over-the-counter medications (e.g. Aspirin), total parenteral nutrition and diet/ vitamin supplements. It may be used to support the order of medication-related devices. It is not intended for use in prescribing particular diets, or for ordering non-medication-related items (eyeglasses, supplies, etc.). In addition, the MedicationRequest may be used to report orders/request from external systems that have been reported for informational purposes and are not authoritative and are not expected to be acted upon (e.g. dispensed or administered). The MedicationRequest resource is a "request" resource from a FHIR workflow perspective - see Workflow Request. The MedicationRequest resource allows requesting only a single medication. If a workflow requires requesting multiple items simultaneously, this is done using multiple instances of this resource. These instances can be linked in different ways, depending on the needs of the workflow. For guidance, refer to the Request pattern **Relationships** The MedicationRequest resource is used to request or order medication for a subject. It may also be used to report a medication request or order from one organization or source to another. When requesting supplies or devices when there is a patient focus or instructions regarding their use, SupplyRequest or DeviceRequest should be used instead. When reporting on the usage of a medication by a patient, the MedicationStatementt resource should be used. The Medication domain includes a number of related resources MedicationRequest | An order for both supply of the medication and the instructions for administration of the medicine to a patient. MedicationDispense | Provision of a supply of a medication with the intention that it is subsequently consumed by a patient (usually in response to a prescription). MedicationAdministration | When a patient actually consumes a medicine, or it is otherwise administered to them MedicationStatement | This is a record of medication being taken by a patient, or that the medication has been given to a patient where the record is the result of a report from the patient, or another clinician. A medication statement is not a part of the prescribe->dispense->administer sequence but is a report that such a sequence (or at least a part of it) did take place resulting in a belief that the patient has received a particular medication. **Referenced By** CarePlan Claim DiagnosticReport ExplanationOfBenefit MedicationAdministration MedicationDispense MedicationStatement Observation ServiceRequest --- ## MedicationStatement import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/medicationstatement.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A record of a medication that is being consumed by a patient. A MedicationStatement may indicate that the patient may be taking the medication now or has taken the medication in the past or will be taking the medication in the future. The source of this information can be the patient, significant other (such as a family member or spouse), or a clinician. A common scenario where this information is captured is during the history taking process during a patient visit or stay. The medication information may come from sources such as the patient's memory, from a prescription bottle, or from a list of medications the patient, clinician or other party maintains. The primary difference between a medication statement and a medication administration is that the medication administration has complete administration information and is based on actual administration information from the person who administered the medication. A medication statement is often, if not always, less specific. There is no required date/time when the medication was administered, in fact we only know that a source has reported the patient is taking this medication, where details such as time, quantity, or rate or even medication product may be incomplete or missing or less precise. As stated earlier, the medication statement information may come from the patient's memory, from a prescription bottle or from a list of medications the patient, clinician or other party maintains. Medication administration is more formal and is not missing detailed information. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Common usage includes: the recording of non-prescription and/or recreational drugs the recording of an intake medication list upon admission to hospital the summarization of a patient's "active medications" in a patient profile A MedicationStatement may be used to record substance abuse or the use of other agents such as tobacco or alcohol. This would typically be done if these substances are intended to be included in clinical decision support checking (for example, interaction checking) and as part of an active medication list. If the intent is to populate social history and/or to include additional information (for example, desire to quit, amount per day, negative health effects), then it is better to record as an Observation that could then be used to populate Social History. This resource does not produce a medication list, but it does produce individual medication statements that may be used in the List resource to construct various types of medication lists. Note that other medication lists can also be constructed from the other Pharmacy resources (e.g., MedicationRequest, MedicationAdministration). A medication statement is not a part of the prescribe -> dispense -> administer sequence, but is a report by a patient, significant other or a clinician that one or more of the prescribe, dispense or administer actions has occurred, resulting is a belief that the patient is, has, or will be using a particular medication. MedicationStatement is an event resource from a FHIR workflow perspective - see Workflow Event **Relationships** The MedicationStatement resource is used to record a medications or substances that the patient reports as being taken, not taking, have taken in the past or may take in the future. It can also be used to record medication use that is derived from other records such as a MedicationRequest. The statement is not used to request or order a medication, supply or device. When requesting medication, supplies or devices when there is a patient focus or instructions regarding their use, a MedicationRequest, SupplyRequest or DeviceRequest should be used instead The Medication domain includes a number of related resources MedicationRequest | An order for both supply of the medication and the instructions for administration of the medicine to a patient. MedicationDispense | Provision of a supply of a medication with the intention that it is subsequently consumed by a patient (usually in response to a prescription). MedicationAdministration | When a patient actually consumes a medicine, or it is otherwise administered to them MedicationStatement | This is a record of a medication being taken by a patient or that a medication has been given to a patient, where the record is the result of a report from the patient or another clinician, or derived from supporting information (for example, Claim, Observation or MedicationRequest). A medication statement is not a part of the prescribe->dispense->administer sequence but is a report that such a sequence (or at least a part of it) did take place, resulting in a belief that the patient has received a particular medication. This resource is distinct from MedicationRequest, MedicationDispense and MedicationAdministration. Each of those resources refers to specific events - an individual order, an individual provisioning of medication or an individual dosing. MedicationStatement is a broader assertion covering a wider timespan and is independent of specific events. The existence of resource instances of any of the preceding three types may be used to infer a medication statement. However, medication statements can also be captured on the basis of other information, including an assertion by the patient or a care-giver, the results of a lab test, etc. **Referenced By** AdverseEvent Goal Observation --- ## MedicinalProduct import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/medicinalproduct.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Detailed definition of a medicinal product, typically for uses other than direct patient care (e.g. regulatory use). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage**
This resource is still undergoing development and review by the appropriate Workgroups. At this time, is considered only as a draft resource not suitable for production implementation
Any pharmaceutical product or combination of pharmaceutical products that may be administered to human beings (or animals) for treating or preventing disease, with the aim/purpose of making a medical diagnosis or to restore, correct or modify physiological functions. A Medicinal Product may contain in the packaging one or more manufactured items and one or more pharmaceutical products. In certain regions a Medicinal Product may also be defined as any substance or combination of substances which may be used to make a medical diagnosis. This applies to proprietary medicinal products for human use intended to be placed on the market and to industrially manufactured medicinal products, the marketing of which has been authorised by a Medicines Regulatory Agency. However, the provisions do not apply to: i) medicinal products prepared according to prescription (e.g. prepared in a pharmacy from a prescription intended for a specific patient), ii) medicinal products prepared in accordance with an official formula (e.g. prepared in a pharmacy in accordance with the instructions in a pharmacopoeia and intended to be given direct to the patient by the pharmacy), iii) medicinal products intended for research and development trials (see 11.2 Investigational Medicinal Product) and to iv) intermediate products intended for subsequent processing by an authorised manufacturer. [from ENV 13607 and ENV 12610] **Referenced By** MedicinalProductAuthorization MedicinalProductContraindication MedicinalProductIndication MedicinalProductInteraction MedicinalProductPackaged MedicinalProductUndesirableEffect --- ## MedicinalProductAuthorization import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/medicinalproductauthorization.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The regulatory authorization of a medicinal product. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage**
This resource is still undergoing development and review by the appropriate Workgroups. At this time, is considered only as a draft resource not suitable for production implementation
MedicinalProductAuthorization is a resource covering the Marketing Authorization of a Medicinal Product, from a regulatory point of view. **Referenced By** MedicinalProductPackaged --- ## MedicinalProductContraindication import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/medicinalproductcontraindication.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The clinical particulars - indications, contraindications etc. of a medicinal product, including for regulatory purposes. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A reason why a medicinal product is not to be used in a certain context --- ## MedicinalProductIndication import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/medicinalproductindication.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Indication for the Medicinal Product. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A medical situation in which a medicinal product is appropriate for use **Referenced By** MedicinalProduct MedicinalProductContraindication --- ## MedicinalProductIngredient import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/medicinalproductingredient.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; An ingredient of a manufactured item or pharmaceutical product. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage**
This resource is still undergoing development and review by the appropriate Workgroups. At this time, is considered only as a draft resource not suitable for production implementation
Material used in the preparation of a medicinal/pharmaceutical product The ingredient is part of a Medicinal Product, either alone or in combination with other ingredients. The ingredient is also a component of a pharmaceutical product. Ingredient is equal to a substance with the indication of the specific role it is playing in the product. **Referenced By** MedicinalProductManufactured MedicinalProductPharmaceutical --- ## MedicinalProductInteraction import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/medicinalproductinteraction.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The interactions of the medicinal product with other medicinal products, or other forms of interactions. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** For detailing potential interactions associated with usage of a medicinal product --- ## MedicinalProductManufactured import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/medicinalproductmanufactured.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The manufactured item as contained in the packaged medicinal product. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Any pharmaceutical product or combination of pharmaceutical products that may be administered to human beings (or animals) for treating or preventing disease, with the aim/purpose of making a medical diagnosis or to restore, correct or modify physiological functions. A Medicinal Product may contain in the packaging one or more manufactured items and one or more pharmaceutical products. In certain regions a Medicinal Product may also be defined as any substance or combination of substances which may be used to make a medical diagnosis. This applies to proprietary medicinal products for human use intended to be placed on the market and to industrially manufactured medicinal products, the marketing of which has been authorised by a Medicines Regulatory Agency. However, the provisions do not apply to: i) medicinal products prepared according to prescription (e.g. prepared in a pharmacy from a prescription intended for a specific patient), ii) medicinal products prepared in accordance with an official formula (e.g. prepared in a pharmacy in accordance with the instructions in a pharmacopoeia and intended to be given direct to the patient by the pharmacy), iii) medicinal products intended for research and development trials (see 11.2 Investigational Medicinal Product) and to iv) intermediate products intended for subsequent processing by an authorised manufacturer. [from ENV 13607 and ENV 12610] **Referenced By** MedicinalProductPackaged --- ## MedicinalProductPackaged import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/medicinalproductpackaged.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A medicinal product in a container or package. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage**
This resource is still undergoing development and review by the appropriate Workgroups. At this time, is considered only as a draft resource not suitable for production implementation
Medicinal Product in a container being part of a package, representing the entirety that has been packaged for sale or supply **Referenced By** MedicinalProduct MedicinalProductAuthorization --- ## MedicinalProductPharmaceutical import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/medicinalproductpharmaceutical.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A pharmaceutical product described in terms of its composition and dose form. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage**
This resource is still undergoing development and review by the appropriate Workgroups. At this time, is considered only as a draft resource not suitable for production implementation
Qualitative and quantitative composition of a Medicinal Product in the dose form approved for administration in line with the regulated product information Note that in many instances, the pharmaceutical product is equal to the manufactured item. However, there are instances where the manufactured item must undergo a transformation before being administered to the patient (as the pharmaceutical product) and the two are not equal. **Referenced By** MedicinalProduct --- ## MedicinalProductUndesirableEffect import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/medicinalproductundesirableeffect.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Describe the undesirable effects of the medicinal product. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A potential negative outcome from the use of a medicinal product. This is part of the definional information about a product and is not realted to any particular actual use of a medication. See also AdverseEvent, which is for actual events (or near misses), and AllergyIntolerance. **Referenced By** MedicinalProductIndication --- ## MessageDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/messagedefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Defines the characteristics of a message that can be shared between systems, including the type of event that initiates the message, the content to be transmitted and what response(s), if any, are permitted. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Implementers should be familiar with the messaging concepts and descriptions found in Messaging using FHIR Resources before working with this resource. **Referenced By** CapabilityStatement MessageHeader --- ## MessageHeader import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/messageheader.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The header for a message exchange that is either requesting or responding to an action. The reference(s) that are the subject of the action as well as other information related to the action are typically transmitted in a bundle in which the MessageHeader resource instance is the first resource in the bundle. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The MessageHeader resource is defined in order to support Messaging using FHIR resources. The principal usage of the MessageHeader resource is when messages are exchanged. However, as a resource that can be used with the RESTful framework, the MessageHeader resource has the normal resource end-point ([base-url]/MessageHeader), which is used to manage a set of static messages resources. This could be used to make an archive of past messages available. Creating or updating Message resources in this fashion does not represent the actual occurrence of any event, nor can it trigger any logic associated with the actual event. It is just for managing a set of message resources. --- ## MolecularSequence import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/molecularsequence.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Raw data describing a biological sequence. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The MolecularSequence resource is designed to describe an atomic sequence which contains the alignment sequencing test result and multiple variations. Atomic sequences can be connected by link element and they will lead to sequence graph. By this method, a sequence can be reported. Complete genetic sequence information, of which specific genetic variations are a part, is reported by reference to the GA4GH repository. Thus, the FHIR MolecularSequence resource avoids large genomic payloads in a manner analogous to how the FHIR ImagingStudy resource references large images maintained in other systems. For use cases, details on how this resource interact with other Clinical Genomics resources or profiles, please refer to implementation guidance document here. Genetic Standards and Resources include: Variant Databases: dbSNP , ClinVar , and COSMIC Reference Sequences: RefSeq and ENSEMBL This resource is designed to describe sequence variations with clinical significance with information such as: Name of the variation represented Type of the variation Gene region occupied by the variation Tissue source used to determine genotype of the variation Quality of the result It is strongly encouraged to provide all available information in this resource for any reported variants, because receiving systems (e.g. discovery research, outcomes analysis, and public health reporting) may use this information to normalize variants over time or across sources. However, these data should not be used to dynamically correct/change variant representations for clinical use outside of the laboratory, due to insufficient information. Implementers should be aware that semantic equivalency of results of genetic variants cannot be guaranteed unless there is an agreed upon standard between sending and receiving systems. **Relationships** Focus of the resource is to provide sequencing alignment data immediately relevant to what the interpretation on clinical decision-making originates from. Hence data such as precise read of DNA sequences and sequence alignment are not included; such data are nonetheless accessible through references to GA4GH (Global Alliance for Genomics and Health) API. The MolecularSequence resource will be referenced by Observation to provide variant information. As clinical assessments/diagnosis of a patient are typically captured in the Condition resource or the ClinicalImpression resource, the MolecularSequence resource can be referenced by the Condition resource to provide specific genetic data to support assertions. This is analogous to how Condition references other resources, such as AllergyIntolerance, Procedure, and Questionnaire resources. **Referenced By** Observation --- ## NamingSystem import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/namingsystem.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A curated namespace that issues unique symbols within that namespace for the identification of concepts, people, devices, etc. Represents a "System" used within the Identifier and Coding data types. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Defines a specific code system or identifier system, so that it can be noted in a registry for other systems to find and understand an identifier. **Relationships** The CodeSystem resource defines the content of a code system, and also its preferred identifier. The NamingSystem resource identifies the existence of a code or identifier system, and its possible and preferred identifiers. The key difference between the resources is who creates and manages them - CodeSystem resources are managed by the owner of the code system resource, who can properly define the features and content of the code system. NamingSystem resources, on the other hand, are frequently defined by 3rd parties that encounter the code system in use, and need to describe the use, but do not have the authority to define the features and content. Additionally, there may be multiple authoritative NamingSystem resources for a code system, but there should only be one CodeSystem resource. **Background and Context** For discussion of policy for creating well maintained OIDs and URIs for resources, see The HL7 Confluence site . --- ## NutritionOrder import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/nutritionorder.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A request to supply a diet, formula feeding (enteral) or oral nutritional supplement to a patient/resident. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is a request resource from a FHIR workflow perspective - see Workflow. It is the intent of the Orders and Observation Workgroup to align this resource with the workflow pattern for request resources. The NutritionOrder resource describes a request for oral diets (including general diets such as General Healthy diet, or therapeutic diets such as Consistent Carbohydrate, 2 gram Sodium, or Fluid Restricted), oral nutrition supplements (such as nutritionally complete pre-packed drinks), enteral nutrition (tube feedings) and infant formula which govern the distribution of food and nutritional products used to feed patients within an in-patient setting. It does not cover orders for parenteral (IV) nutrition which are typically filled by pharmacy. These nutrition orders are combined with information on a patient's food allergies and intolerances, and ethnic or cultural food preferences (e.g. Kosher or Vegetarian) to inform healthcare personnel about the type, texture, and/or quantity of foods that the patient should receive or consume. Enteral orders are distinguished from supplements because they have some unique attributes and typically include administration information whereas oral nutritional supplements may simply be supplied (e.g. home health or outpatient settings). In a simple case, the requestor may designate type of product, product name and the route of administration along with free text instructions without a having to complete the additional structured details. This resource is intended to be used by providers from a variety of specialties such as physicians, dietitian/nutritionists, or speech therapists. One provider may simply order a base element oral diet such as General Healthful diet. Another provider, based on scope of practice, may use other elements to communicate additional therapeutic needs or patient preferences. The optionality included gives an ordering provider the capability to write a simple order for an oral diet, nutritional supplement or formula with minimal requirements beyond that of specifying the diet, supplement or formula product, but also supports the ability to provide more detailed information that may be further augmented by a dietitian or nutrition specialist. For example, a physician may order a 2 g sodium diet. A speech therapist, based on the results of a swallowing evaluation, then orders a mechanically altered texture with nectar thick liquids. **Relationships** The NutritionOrder resource is used for requesting oral diets, oral nutrition supplements and enteral feedings in an in-patient setting. The MedicationRequest resource should be used for requesting parenteral (IV) nutrition and prescribing dietary supplements such as vitamin or mineral supplements. The Nutrition Order is a record of the request for the supply of a diet, oral supplement or enteral formulas for a patient. However, to initiate the request requires the use of the Task resource and its associated workflow with the Nutrition Order referenced from Task.basedOn, or by using the Nutrition Task resource in the context of a messaging or service workflow where the request is explicit or implicit. **Referenced By** CarePlan DiagnosticReport Goal Observation --- ## Observation import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/observation.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Measurements and simple assertions made about a patient, device or other subject. US Core Profiles related to Observations are as follows: - [US Core Laboratory Result profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-observation-lab.html) - [US Core Smoking Status Observation profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-smokingstatus.html). - [US Core Pulse Oximetry Profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-pulse-oximetry.html) - [US Core Pediatric BMI for Age Profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-pediatric-bmi-for-age.html) - [US Core Pediatric Weight for Height](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-pediatric-weight-for-height.html) - [US Core Pediatric Head Circumference](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-head-occipital-frontal-circumference-percentile.html) - [US Core Observation Survey](http://hl7.org/fhir/us/core/StructureDefinition-us-core-observation-survey.html) - [US Core Observation Social History](http://hl7.org/fhir/us/core/StructureDefinition-us-core-observation-social-history.html) - [US Core Clinical Test Result](http://hl7.org/fhir/us/core/StructureDefinition-us-core-observation-clinical-test.html) - [US Core Observation Sexual Orientation](http://hl7.org/fhir/us/core/StructureDefinition-us-core-observation-sexual-orientation.html) **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is an event resource from a FHIR workflow perspective - see Workflow. Observations are a central element in healthcare, used to support diagnosis, monitor progress, determine baselines and patterns and even capture demographic characteristics. Most observations are simple name/value pair assertions with some metadata, but some observations group other observations together logically, or even are multi-component observations. Note that the DiagnosticReport resource provides a clinical or workflow context for a set of observations and the Observation resource is referenced by DiagnosticReport to represent laboratory, imaging, and other clinical and diagnostic data to form a complete report. Uses for the Observation resource include: Vital signs such as body weight, blood pressure, and temperature Laboratory Data like blood glucose, or an estimated GFR Imaging results like bone density or fetal measurements Clinical Findings† such as abdominal tenderness Device measurements such as EKG data or Pulse Oximetry data Clinical assessment tools such as APGAR or a Glasgow Coma Score Personal characteristics: such as eye-color Social history like tobacco use, family support, or cognitive status Core characteristics like pregnancy status, or a death assertion †The boundaries between clinical findings and disorders remains a challenge in medical ontology. Refer the Boundaries section below and in Condition for general guidance. These boundaries can be clarified by profiling Observation for a particular use case. Core Profiles for Observation Trial Use The following core profiles for the Observation resource have been defined as well. If implementations use this Resource when expressing the profile-specific concepts as structured data, they SHALL conform to the following profiles: Profile | Description Vital signs | The FHIR Vital Signs profile sets minimum expectations for the Observation Resource to record, search and fetch the vital signs (e.g. temperature, blood pressure, respiration rate, etc.) associated with a patient **Relationships** At its core, Observation allows expressing a name-value pair or structured collection of name-value pairs. As such, it can support conveying any type of information desired. However, that is not its intent. Observation is intended for capturing measurements and subjective point-in-time assessments. It is not intended to be used for those specific contexts and use cases already covered by other FHIR resources. For example, the AllergyIntolerance resource represents a patient allergies, MedicationStatement resource: medications taken by a patient, FamilyMemberHistory resource: a patient's family history, Procedure resource: information about a procedure, and QuestionnaireResponse resource: a set of answers to a set of questions. The Observation resource should not be used to record clinical diagnosis about a patient or subject that are typically captured in the Condition resource or the ClinicalImpression resource. The Observation resource is often referenced by the Condition resource to provide specific subjective and objective data to support its assertions. There will however be situations of overlap. For example, a response to a question of "have you ever taken illicit drugs" could in principle be represented using MedicationStatement, but most systems would treat such an assertion as an Observation. In some cases, such as when source data is coming from an HL7 v2 feed, a system might not have information that allows it to distinguish diagnosis, allergy and other "specialized" types of observations from laboratory, vital sign and other observation types intended to be conveyed with this resource. In those circumstances, such specialized observations may also appear using this resource. Adhering to such convention is an appropriate use of Observation. If implementers are uncertain whether a proposed use of Observation is appropriate, they're encouraged to consult with implementers on chat.fhir.org implementer's stream The Media resource captures a specific type of observation whose value is audio, video or image data. This resource is used instead of Observation to represent such forms of information as it exposes the metadata relevant for interpreting the information. See Media's boundaries section to see how Media (and Observation) differs from ImagingStudy and DocumentReference. In contrast to the Observation resource, the DiagnosticReport resource typically includes additional clinical context and some mix of atomic results, images, imaging reports, textual and coded interpretation, and formatted representations. Laboratory reports, pathology reports, and imaging reports should be represented using the DiagnosticReport resource. The Observation resource is referenced by the DiagnosticReport to provide the atomic results for a particular investigation. "Laboratories routinely have a variable that is summative across a series of discrete variables - these are usually called 'impressions' or 'interpretations'. Sometimes they are algorithmically specified and sometimes they have the imprimatur of pathologists and they are conveyed in Observation or DiagnosticReport instead of the Clinical Impression resource. The Observation resource should not be used to record clinical diagnosis about a patient or subject as discussed above. **Referenced By** AdverseEvent Appointment CarePlan ChargeItem ClinicalImpression Communication CommunicationRequest Condition Contract DeviceRequest DeviceUseStatement DiagnosticReport Encounter FamilyMemberHistory Goal GuidanceResponse ImagingStudy Immunization MedicationAdministration MedicationRequest MedicationStatement MolecularSequence Procedure QuestionnaireResponse RequestGroup RiskAssessment ServiceRequest SupplyRequest --- ## ObservationDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/observationdefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Set of definitional characteristics for a kind of observation or measurement produced or consumed by an orderable health care service. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage**
Note that this content is preliminary has not undergone proper review by the appropriate Workgroups.
An instance of ObservationDefinition represents the definitional aspects of a kind of observation. This resource is used in a catalog of products or services for healthcare. It provides the definition of observations, which are used as input or produced as outcome of these products and services. The focus of the definitional aspects of ObservationDefinition is not on the action of performing observations of that kind. Rather, it is on the outcome of the observations. An instance of ObservationDefinition represents a set of constraints applicable to the value produced by observations of this kind. This set of constraints may have two utilities: 1) It guides the entry of the result of an observation of the kind defined. 2) It helps the consumer of the results of observations of that kind, to interpret properly these results. For instance, in a catalog of laboratory in vitro diagnostic services, the ObservationDefinition resources describe the observations that should be provided by the orderer alongside with an ordered service (questions asked at order entry or at specimen collection, prior results) as well as the observations that will be reported once the service has been performed. In a catalog of medications, ObservationDefinition resources can be used to describe observations the result thereof need to be checked against the prescription of a particular medicinal product (therapeutic range, units ...) In the catalog of tests of an in vitro diagnostic device, ObservationDefinition resources can be used to characterize (absolute range, unit, decimal precision, critical values ...) the observations that will be produced by the IVD tests performed by the device. ObservationDefinition may be used to characterize observations from any discipline. **Relationships** This resource relates to these other resource:. Resource ObservationDefinition is referenced by resource CatalogEntry. This enables items of catalogs, representing healthcare services or products, to declare the observations, which are consumed or produced by these items. Observation.basedOn references ObservationDefinition resource. **Referenced By** ActivityDefinition CatalogEntry MedicationKnowledge MedicinalProductInteraction --- ## OperationDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/operationdefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A formal computable definition of an operation (on the RESTful interface) or a named query (using the search interaction). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The OperationDefinition resource provides a formal computable definition of an operation or a named query. The OperationDefinition serves two principal purposes: To allow for automatic determination of system compatibility To allow for dynamic generation of forms to drive the operations See below for further information about these, and about how operations and named queries are executed. **Relationships** OperationDefinitions are published to define operations that servers can implement in a common fashion. The FHIR specification itself describes some (see below), and other organizations (including IHE, national programs, jurisdictions and vendors) are able to publish additional OperationDefinitions. OperationDefinition resources are referred to from two different places: From a CapabilityStatement, to declare what operations a system does or should implement From another OperationDefinition resource. This allows for a server to describe a limited implementation of a standard operation, or to allow traceability if the server has to rename the operation due to a name clash **Referenced By** CapabilityStatement CarePlan Communication FamilyMemberHistory Procedure --- ## OperationOutcome import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/operationoutcome.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A collection of error, warning, or information messages that result from a system action. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Operation outcomes are sets of error, warning and information messages that provide detailed information about the outcome of an attempted system operation. They are provided as a direct system response, or component of one, and provide information about the outcome of the operation. The OperationOutcome resource is used in the following circumstances: When a RESTful interaction or operation fails As the response on a validation operation to provide information about the outcome As part of a message response, usually when the message has not been processed correctly As the response to a batch/transaction, when requested As part of a search's Bundle response containing information about the search **Relationships** This resource is not used for reporting clinical or workflow issues associated with a proposed or ongoing action; these would be handled using DetectedIssue or other resources. OperationOutcome is not designed to be persisted or referenced from other parts of the workflow. It is possible to have both OperationOutcome and DetectedIssue together, where the OperationOutcome might indicate that a requested action was rejected due to a clinical issue and the DetectedIssue provides the details of the issue. **Referenced By** GuidanceResponse MessageHeader --- ## Organization import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/organization.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A formally or informally recognized grouping of people or organizations formed for the purpose of achieving some form of collective action. Includes companies, institutions, corporations, departments, community groups, healthcare practice groups, payer/insurer, etc. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource may be used in a shared registry of contact and other information for various organizations or it can be used merely as a support for other resources that need to reference organizations, perhaps as a document, message or as a contained resource. If using a registry approach, it's entirely possible for multiple registries to exist, each dealing with different types or levels of organization. **Relationships** The Organization resource is used for collections of people that have come together to achieve an objective. The Group resource is used to identify a collection of people (or animals, devices, etc.) that are gathered for the purpose of analysis or acting upon, but are not expected to act themselves. The Organization resource often exists as a hierarchy of organization resources, using the part-of property to provide the association of the child to its parent organization. This organizational hierarchy helps communicate the conceptual structure, whereas the Location resource provides the physical representation of the hierarchy. The linkage between Organization and Location is from each point in the location hierarchy to the appropriate level in the Organization hierarchy. These links don't all have to be to the top level Organization. When populating the organization and location hierarchies there is often not a clear distinction between these 2, however to assist in making the decision, Locations are always used for recording where a service occurs, and hence where encounters and observations are associated. The Organization property on these resources might not be the location where the service took place. **Referenced By** Annotation Identifier Signature UsageContext Account AuditEvent Basic BiologicallyDerivedProduct CapabilityStatement CarePlan CareTeam CatalogEntry ChargeItem Claim ClaimResponse Communication CommunicationRequest Composition Consent Contract Coverage CoverageEligibilityRequest CoverageEligibilityResponse Device DeviceDefinition DeviceRequest DiagnosticReport DocumentManifest DocumentReference Encounter Endpoint EnrollmentRequest EnrollmentResponse EpisodeOfCare ExplanationOfBenefit Flag Goal Group HealthcareService ImagingStudy Immunization ImmunizationEvaluation ImmunizationRecommendation InsurancePlan Invoice Linkage Location MeasureReport Media Medication MedicationDispense MedicationKnowledge MedicationRequest MedicationStatement MedicinalProduct MedicinalProductAuthorization MedicinalProductIngredient MedicinalProductManufactured MedicinalProductPackaged MessageHeader MolecularSequence Observation OrganizationAffiliation Patient PaymentNotice PaymentReconciliation Person Practitioner PractitionerRole Procedure Provenance QuestionnaireResponse ResearchStudy ServiceRequest SupplyDelivery SupplyRequest Task VerificationResult --- ## OrganizationAffiliation import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/organizationaffiliation.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Defines an affiliation/assotiation/relationship between 2 distinct oganizations, that is not a part-of relationship/sub-division relationship. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A relationship between 2 organizations over a period of time, where the entities are separate business entities. The relationship can optionally include details of locations/services from the participating organization. The OrganizationAffiliation enables defining non-hierarchical relationships between organizations. For example: One organization may provide services to another organization e.g. An agency service providing casual staff, a radiology service, a diagnostic lab, catering services, community care services etc. Two or more organizations may form a partnership or joint venture An organization may be a member of an association, but not owned by it e.g. a hospital is a member the American Hospital Association, a hospital is a member of a health information exchange network **Relationships** The Organization.partOf is used to form a hierarchical relationships within an organization which eventually resolves to a single organization. Each child in the tree is a subdivision of the parent. The OrganizationAffiliation is used to describe the relationship between two distinct organizations. It does not require a hierarchical relationship. A resource instance is for a singular linkage between 2 organizations (it does not contain all members), to create multiple affiliations, create additional resources for the other relationship between other organizations. Each instance contains its own period, and optionally links to specific related services/locations that are available. These referenced locations/services should be associated with the participating organization, and are available to the primary organization. This resource should not be used when the affiliates are part of a single organization. --- ## Parameters import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/parameters.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; This resource is a non-persisted resource used to pass information into and back from an [operation](/docs/api/fhir/operations). It has no other use, and there is no RESTful endpoint associated with it. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is a non-persisted resource used to pass information into and back from an operation. **Relationships** This special resource has no other use than for operation parameters, and there is no RESTful end-point associated with it. For further information, see the operations page. OperationDefinition defines constraints on the Parameters resource instances that are used to convey the inputs and outputs of the operation. **Referenced By** GuidanceResponse --- ## Patient import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/patient.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Demographics and other administrative information about an individual or animal receiving care or other health-related services. Refer to the following profiles for information on representing specific details: - [US Core Patient profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-patient.html). - [US Core Gender Identity Extension](http://hl7.org/fhir/us/core/StructureDefinition-us-core-genderIdentity.html). - [US Core Birth Sex Extension](http://hl7.org/fhir/us/core/StructureDefinition-us-core-birthsex.html). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This Resource covers data about patients and animals involved in a wide range of health-related activities, including: Curative activities Psychiatric care Social services Pregnancy care Nursing and assisted living Dietary services Tracking of personal health and exercise data The data in the Resource covers the "who" information about the patient: its attributes are focused on the demographic information necessary to support the administrative, financial and logistic procedures. A Patient record is generally created and maintained by each organization providing care for a patient. A patient or animal receiving care at multiple organizations may therefore have its information present in multiple Patient Resources. Not all concepts are included within the base resource (such as race, ethnicity, organ donor status, nationality, etc.), but may be found in profiles defined for specific jurisdictions (e.g., US Meaningful Use Program) or standard extensions. Such fields vary widely between jurisdictions and often have different names and valuesets for the similar concepts, but they are not similar enough to be able to map and exchange. **Referenced By** Annotation Signature Account AdverseEvent AllergyIntolerance Appointment AppointmentResponse AuditEvent Basic BiologicallyDerivedProduct BodyStructure CarePlan CareTeam ChargeItem Claim ClaimResponse ClinicalImpression Communication CommunicationRequest Composition Condition Consent Contract Coverage CoverageEligibilityRequest CoverageEligibilityResponse DetectedIssue Device DeviceRequest DeviceUseStatement DiagnosticReport DocumentManifest DocumentReference Encounter EnrollmentRequest EpisodeOfCare ExplanationOfBenefit FamilyMemberHistory Flag Goal Group GuidanceResponse ImagingStudy Immunization ImmunizationEvaluation ImmunizationRecommendation Invoice List MeasureReport Media MedicationAdministration MedicationDispense MedicationRequest MedicationStatement MolecularSequence NutritionOrder Observation Person Procedure Provenance QuestionnaireResponse RelatedPerson RequestGroup ResearchSubject RiskAssessment Schedule ServiceRequest Specimen SupplyDelivery SupplyRequest Task VisionPrescription --- ## PaymentNotice import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/paymentnotice.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; This resource provides the status of the payment for goods and services rendered, and the request and response resource references. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The PaymentNotice resource indicates the resource for which the payment has been indicated and reports the current status information of that payment. The payment notice may be used by providers and payers to advise the provider or regulatory bodies of the state of a payment (check in the mail/EFT sent, payment cashed, payment canceled). Employers or Insurance Exchanges may use this to advise payers of premium payment. Payers and/or providers may have the need to advise providers and/or regulators of the status of claim settlement and payment completion. This same resource may be used by employers and insurance exchanges to advise payers that premium payments have been issued and settled. The PaymentNotice resource is a "event" resource from a FHIR workflow perspective - see Workflow Event. Additional Information Additional information regarding electronic payment notice content and usage may be found at: Financial Resource Status Lifecycle: how .status is used in the financial resources. Batches: how eClaims may handle batches of eligibility, claims, payments and responses. **Relationships** The PaymentNotice resource is used to indicate that a payment is, or is about to occur. The resource is used commonly when tracking of payment occurences is required for regulatory or business purposes, such as ensuring that payment of claim adjudications occurs within a specific timeframe. The PaymentNotice is not used to report adjudicated amounts (see ClaimResponse) or to reconcile adjudicated amounts against actual payment (see PaymentReconciliation) The ClaimResponse resource is an insurer's adjudication and/or authorization of a set of healthcare-related goods and services for a patient against the patient's insurance coverages and does not indicate that payment has occured. The PaymentReconciliation resource serves to correlate payment amounts to the adjudicated claim amounts appearing on one or more ClaimResponses. The eClaim domain includes a number of related resources PaymentNotice | An indication that a payment has occured, is about to occur, or has been cancelled. ClaimResponse | A payor's adjudication and/or authorization response to the suite of services provided in a Claim. Typically the ClaimResponse references the Claim but does not duplicate the clinical or financial information provided in the claim. PaymentReconciliation | Provides the bulk payment details associated with a payment by the payor for goods and services rendered by a provider to patients covered by insurance plans offered by that payor. --- ## PaymentReconciliation import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/paymentreconciliation.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; This resource provides the details including amount of a payment and allocates the payment items being paid. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The PaymentReconciliation resource provides the bulk payment details associated with a payment by the payor for receivable amounts, such as for goods and services rendered by a provider to patients covered by insurance plans offered by that payor. These are the payment reconciliation details which may align to the individual payment amounts indicated on discrete ClaimResponses or Invoices for example. Bulk payments need to provide a means to associate the amounts paid against specific Claims, Invoices, and other financial exchanges and adjustments, to the bulk payment itself in order to reconcile accounts receivable. The PaymentReconciliation resource is a "event" resource from a FHIR workflow perspective - see Workflow Event. Additional Information Additional information regarding electronic payment notice content and usage may be found at: Financial Resource Status Lifecycle: how .status is used in the financial resources. **Relationships** The PaymentReconciliation resource serves to correlate a payment amount to the adjudicated claim amounts appearing on one or more ClaimResponses. PaymentReconciliation provides the bulk payment details associated with a payment by the payor for goods and services rendered by a provider to patients covered by insurance plans offered by that payor. The PaymentNotice resource is used to indicate that a payment has, or is about to occur. The resource is used commonly when tracking of payment occurences is required for regulatory or business purposes. The ClaimResponse resource is an insurer's adjudication and/or authorization of a set of healthcare-related goods and services for a patient against the patient's insurance coverages and does not indicate that payment has occured. For reporting out to patients or transferring data to patient centered applications, such as patient health Record (PHR) application, the ExplanationOfBenefit should be used instead. The eClaim domain includes a number of related resources PaymentReconciliation | Provides the bulk payment details associated with a payment by the payor for goods and services rendered by a provider to patients covered by insurance plans offered by that payor. ClaimResponse | A payor's adjudication and/or authorization response to the suite of services provided in a Claim. Typically the ClaimResponse references the Claim but does not duplicate the clinical or financial information provided in the claim. ExplanationOfBenefit | This resource combines the information from the Claim and the ClaimResponse, stripping out any provider or payor proprietary information, into a unified information model suitable for use for: patient reporting; transferring information to a Patient Health Record system; and, supporting complete claim and adjudication information exchange with regulatory and analytics organizations and other parts of the provider's organization. PaymentNotice | An indication that a payment has occured, is about to occur, or has been cancelled. **Referenced By** PaymentNotice --- ## Person import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/person.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Demographics and administrative information about a person independent of a specific health-related context. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The Person resource serves as a linkage resource that may provide a reference set of common demographics for an individual (human or animal) across multiple roles. This linkage can be direct to role-specific FHIR resources (Patient, Practitioner and RelatedPerson) residing on the same or possibly distinct FHIR systems/applications, or indirectly through the use of business identifiers. The Person resource may be used in many situations/contexts, including: A set of demographics that can be used to co-ordinate the maintenance of this de-normalized information across practitioners, patients and/or related persons e.g. link known resources of the different types together within a system A state/network based Master Person Index e.g. A National Identifier Index or a Network membership/subscriber list A central register that links patient resources from multiple servers, indicating they all correspond to the same individual e.g. Within a large organization with many systems to be able to link the various records, without having to modify the source information with external links A support for access monitoring software that is able to assert what practitioner, patient and related person records correspond to the same human being to assist in detecting improper querying. e.g. asserting that a Patient A is the same individual as a RelatedPerson B who is related to Patient C, who is also Practitioner D. As such, monitoring for Practitioner D’s accesses to Patient A’s records should be carefully scrutinized. Local record(s) on a mobile device listing links to remote server patient resources e.g. A mobile phone application storing references to the hospitals they have access to
Note: The Person resource is an advanced feature. Many systems don’t have a way to relate information across resource types, or systems, especially from Patient to Practitioner, and therefore might not implement this functionality. Some do have ways of relating the Patient and RelatedPerson resource types. FHIR is also able to do this without the Person resource, utilizing the Patient.link property (as shown in the Mother and newborn relationship example)
**Relationships** Person resources may have references to Patient, RelatedPerson and/or Practitioner resources. These linkages assert that all the records relate to the same individual. Person instances are never directly referenced as actors (authors, subjects, performers, etc.) Individual actors are always identified as either Patient, Practitioner or RelatedPerson, depending on the role of the individual when undertaking the action. Therefore this resource SHALL NOT be referenced by any other clinical or administrative resources. E.g. it can be the focus of an operation or message or included in a Bundle, but cannot be a member of a List or Group, the subject of an Observation, or other similar non-infrastructural usage. In some use cases the linkages will be indirect, and through business Identifiers, such as in the case of the various types of Master Person Indexes, where the records are not there for the purpose of healthcare, but simply describing a population of people, and has other purposes and not just to represent a population patients. --- ## PlanDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/plandefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; This resource allows for the definition of various types of plans as a sharable, consumable, and executable artifact. The resource is general enough to support the description of a broad range of clinical artifacts such as clinical decision support rules, order sets and protocols. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is a definition resource from a FHIR workflow perspective - see Workflow, specifically Definition. A plan definition is a pre-defined group of actions to be taken in particular circumstances, often including conditional elements, options, and other decision points. The resource is flexible enough to be used to represent a variety of workflows, as well as clinical decision support and quality improvement assets, including order sets, protocols, and decision support rules. PlanDefinitions can contain hierarchical groups of action definitions, where each action definition describes an activity to be performed (often in terms of an ActivityDefinition resource), and each group defines additional behavior, relationships, and applicable conditions between the actions in the overall definition. In addition to describing what should take place, each action in a plan definition can specify when and whether the action should take place. For when the action should be taken, the trigger element specifies the action should be taken in response to some trigger occurring (such as a particular point in a workflow being reached, or as the result of a prescription being ordered). For whether the action should be taken, the condition element can be used to provide an expression that evaluates to true or false to indicate the applicability of the action to the specific context. The process of applying a PlanDefinition to a particular context typically produces request resources representing the actions that should be performed, typically grouped within a CarePlan and/or RequestGroup to capture relationships between the resulting request resources. Each ActivityDefinition is used to construct a specific resource, based on the definition of the activity and combined with contextual information for the particular patient that the plan definition is being applied to. As with the ActivityDefinition, a PlanDefinition may provide information about how to transform the activity to a specific intent resource, either by specifying a StructureMap that can be used to perform the transformation completely, or by specifying values for specific elements of the resulting resource using dynamicValue elements in the action. Note that these mechanisms are provided on both the ActivityDefinition and the PlanDefinition to allow both reusable transformation descriptions, as well as customization of those descriptions within specific contexts. As such, the transform descriptions specified on the PlanDefinition override transform descriptions defined on the ActivityDefinition. Dynamic values within the definitions can be provided by specifying the expression directly, or by referencing an expression defined within a library. For more information on how to reference expressions within resources, refer to the Using Expressions topic. As an example, the Low Suicide Risk example order set from the Clinical Decision Support Knowledge Artifact Specification can be represented using the PlanDefinition and ActivityDefinition structures: Low Suicide Risk Example Order Set. In addition to the representation of PlanDefinitions, the $apply operation allows PlanDefinitions to be applied to a specific context such as a patient, practitioner, or institution. For Order Sets specifically, this operation is expected to place the orders defined by the order set, consistent with the service functional requirements defined by the Order Set specification . **Relationships** The PlanDefinition resource is used to describe series, sequences, or groups of actions to be taken, while the ActivityDefinition resource is used to define each specific step or activity to be performed. As the name implies, the PlanDefinition resource is strictly definitional. It does not represent the intention to take any action, nor does it represent that any actions have been taken. Rather, the resource provides a definition that can be applied in the appropriate circumstances. When the plan definition is applied, the result will in general be a set of actions that should be (or potentially even have been) performed. Note that the PlanDefinition still has action-level information, as well as a reference to an ActivityDefinition. The action-level information defined in the PlanDefinition itself is used to describe how the actions are related to each other within the plan, where the ActivityDefinition contains only information about the activity itself. In addition, there is some overlapping information that allows the resources to be used independently, or in combination. See the Applying a PlanDefinition section for more information. **Referenced By** UsageContext CarePlan CatalogEntry Communication DeviceRequest FamilyMemberHistory Flag MessageDefinition NutritionOrder Procedure ResearchStudy ServiceRequest --- ## Practitioner import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/practitioner.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A person who is directly or indirectly involved in the provisioning of healthcare. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Practitioner covers all individuals who are engaged in the healthcare process and healthcare-related services as part of their formal responsibilities and this Resource is used for attribution of activities and responsibilities to these individuals. Practitioners include (but are not limited to): physicians, dentists, pharmacists physician assistants, nurses, scribes midwives, dietitians, therapists, optometrists, paramedics medical technicians, laboratory scientists, prosthetic technicians, radiographers social workers, professional homecare providers, official volunteers receptionists handling patient registration IT personnel merging or unmerging patient records Service animal (e.g., ward assigned dog capable of detecting cancer in patients) **Relationships** The Resource SHALL NOT be used for persons involved without a formal responsibility like individuals taking care for friends, relatives or neighbors. These can be registered as a Patient's Contact. If performing some action or being referenced by another resource, use the RelatedPerson resource. The primary distinction between a Practitioner and a RelatedPerson is based on whether: The person/animal operates on behalf of the care delivery organization over multiple patients (Practitioner) or, Where the person/animal is not associated with the organization, and instead is allocated tasks specifically for the RelatedPerson's Patient (RelatedPerson). A standard extension animalSpecies can be used to indicate the species of a service animal. The PractitionerRole resource provides the details of roles that the practitioner is approved to perform for which organizations (and at which locations, and optionally what services too). Practitioners are also often grouped into CareTeams independently of roles, where the CareTeam defines what specific role that they are fulfilling within the team, and might or might not have actual practitioner role resources created for the practitioner (and in the care team context, the organization the practitioner is representing) **Background and Context** Practitioner performs different roles within the same or even different organizations. Depending on jurisdiction and custom, it may be necessary to maintain a specific Practitioner Resource for each such role or have a single Practitioner with multiple roles. The role can be limited to a specific period, after which authorization for this role ends. Note that the represented organization need not necessarily be the (direct) employer of a Practitioner. **Referenced By** Annotation Signature Account AdverseEvent AllergyIntolerance Appointment AppointmentResponse AuditEvent Basic BiologicallyDerivedProduct CarePlan CareTeam CatalogEntry ChargeItem Claim ClaimResponse ClinicalImpression Communication CommunicationRequest Composition Condition Consent Contract CoverageEligibilityRequest CoverageEligibilityResponse DetectedIssue DeviceRequest DeviceUseStatement DiagnosticReport DocumentManifest DocumentReference Encounter EnrollmentRequest EnrollmentResponse EpisodeOfCare ExplanationOfBenefit Flag Goal Group ImagingStudy Immunization Invoice Linkage List MeasureReport Media MedicationAdministration MedicationDispense MedicationRequest MedicationStatement MessageHeader NutritionOrder Observation Patient PaymentNotice PaymentReconciliation Person PractitionerRole Procedure Provenance QuestionnaireResponse RequestGroup ResearchStudy RiskAssessment Schedule ServiceRequest Specimen SupplyDelivery SupplyRequest Task VerificationResult VisionPrescription --- ## PractitionerRole import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/practitionerrole.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A specific set of Roles/Locations/specialties/services that a practitioner may perform at an organization for a period of time. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** PractitionerRole covers the recording of the location and types of services that Practitioners are able to provide for an organization. The role, specialty, Location telecom and HealthcareService properties can be repeated if required in other instances of the PractitionerRole. Some systems record a collection of service values for a single location, others record the single service and the list of locations it is available. Both are acceptable options for representing this data. Where availability, telecom, or other details are not the same across all healthcareservices, or locations a seperate PractitionerRole instance should be created. **Relationships** Qualifications (from the Practitioner resource) do not imply a Role, but might be considered when an Organization allocates practitioners to roles within their organization, and could provide useful information (such as expiry information) which could need to be tracked in some situations to ensure they continue to be eligible for a specific role. The CareTeam resource is also often used to provide details of a role that a practitioner is allocated to perform, but is usually scoped to a much finer granularity of care, and often within the specific context of a Patient, or functional role (e.g. Crisis planning team). In contrast the PractitionerRole is used in a more general sense to cover all the places that the practitioner is allocated to work (and specific details relevant to that role - such as a specific contact number, or electronic services endpoint). **Background and Context** Practitioner performs different roles within the same or even different organizations. Depending on jurisdiction and custom, it may be necessary to maintain a specific Practitioner Resource for each such role or have a single Practitioner with multiple roles. The role can be limited to a specific period, after which authorization for this role ends. Note that the represented organization need not necessarily be the (direct) employer of a Practitioner. **Referenced By** Signature Account AdverseEvent AllergyIntolerance Appointment AppointmentResponse AuditEvent Basic BiologicallyDerivedProduct CarePlan CareTeam CatalogEntry ChargeItem Claim ClaimResponse ClinicalImpression Communication CommunicationRequest Composition Condition Consent Contract CoverageEligibilityRequest CoverageEligibilityResponse DetectedIssue DeviceRequest DeviceUseStatement DiagnosticReport DocumentManifest DocumentReference Encounter EnrollmentRequest EnrollmentResponse EpisodeOfCare ExplanationOfBenefit Flag Goal Group ImagingStudy Immunization Invoice Linkage List MeasureReport Media MedicationAdministration MedicationDispense MedicationRequest MedicationStatement MedicinalProduct MessageHeader NutritionOrder Observation Patient PaymentNotice PaymentReconciliation Procedure Provenance QuestionnaireResponse RequestGroup ResearchStudy RiskAssessment Schedule ServiceRequest Specimen SupplyDelivery SupplyRequest Task VerificationResult VisionPrescription --- ## Procedure import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/procedure.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; An action that is or was performed on or for a patient. This can be a physical intervention like an operation, or less invasive like long term services, counseling, or hypnotherapy. Refer to the [US Core Procedure profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-procedure.html). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Procedure is one of the event resources in the FHIR workflow specification. This resource is used to record the details of current and historical procedures performed on or for a patient. A procedure is an activity that is performed on, with, or for a patient as part of the provision of care. Examples include surgical procedures, diagnostic procedures, endoscopic procedures, biopsies, counseling, physiotherapy, personal support services, adult day care services, non-emergency transportation, home modification, exercise, etc. Procedures may be performed by a healthcare professional, a service provider, a friend or relative or in some cases by the patient themselves. This resource provides summary information about the occurrence of the procedure and is not intended to provide real-time snapshots of a procedure as it unfolds, though for long-running procedures such as psychotherapy, it could represent summary level information about overall progress. The creation of a resource to support detailed real-time procedure information awaits the identification of a specific implementation use-case to share such information. **Relationships** The Procedure resource should not be used to capture an event if a more specific resource already exists - i.e. immunizations, drug administrations and communications. The boundary between determining whether an action is a Procedure (training or counseling) as opposed to a Communication is based on whether there's a specific intent to change the mind-set of the patient. Mere disclosure of information would be considered a Communication. A process that involves verification of the patient's comprehension or to change the patient's mental state would be a Procedure. Note that many diagnostic processes are procedures that generate Observations and DiagnosticReports. In many cases, such an observation does not require an explicit representation of the procedure used to create the observation, but where there are details of interest about how the diagnostic procedure was performed, the Procedure resource is used to describe the activity. Some diagnostic procedures might not have a Procedure record. The Procedure record is only necessary when there is a need to capture information about the physical intervention that was performed to capture the diagnostic information (e.g. anesthetic, incision, scope size, etc.) A Task is a workflow step such as cancelling an order, fulfilling an order, signing an order, merging a set of records, admitting a patient. Procedures are actions that are intended to result in a physical or mental change to or for the subject (e.g. surgery, physiotherapy, training, counseling). A Task resource often exists in parallel with clinical resources. For example, a Task might request fulfillment of a ServiceRequest ordering a Procedure. **Referenced By** AdverseEvent Appointment ChargeItem Claim DeviceUseStatement Encounter ExplanationOfBenefit Flag ImagingStudy MedicationAdministration MedicationDispense MedicationStatement Observation QuestionnaireResponse --- ## Provenance import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/provenance.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Provenance of a resource is a record that describes entities and processes involved in producing and delivering or otherwise influencing that resource. Provenance provides a critical foundation for assessing authenticity, enabling trust, and allowing reproducibility. Provenance assertions are a form of contextual metadata and can themselves become important records with their own provenance. Provenance statement indicates clinical significance in terms of confidence in authenticity, reliability, and trustworthiness, integrity, and stage in lifecycle (e.g. Document Completion - has the artifact been legally authenticated), all of which may impact security, privacy, and trust policies. Refer to the [US Core Provenance profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-provenance.html). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The Provenance resource tracks information about the activity that created, revised, deleted, or signed a version of a resource, describing the entities and agents involved. This information can be used to form assessments about its quality, reliability, trustworthiness, or to provide pointers for where to go to further investigate the origins of the resource and the information in it. Provenance resources are a record-keeping assertion that gathers information about the context in which the information in a resource was obtained. Provenance resources are prepared by the application that initiates the create/update etc. of the resource. An AuditEvent resource contains overlapping information, but is created as events occur, to track and audit the events. AuditEvent resources are often (though not exclusively) created by the application responding to the read/query/create/update/etc. event. **Relationships** Many other FHIR resources contain some elements that represent information about how the resource was obtained, and therefore they overlap with the functionality of the Provenance resource. These properties in other resources should always be used in preference to the Provenance resource, and the Provenance resource should be used where additional information is required, or explicit record or provenance is desired. The relationship between a resource and its provenance is established by a reference from the provenance resource to its target. In this way, provenance may be provided about any resource or version, including past versions. There may be multiple provenance records for a given resource or version of a resource. **Background and Context** The Provenance resource is based on the W3C Provenance specification , and mappings are provided. The Provenance resource is tailored to fit the FHIR use-cases for provenance more directly. In terms of W3C Provenance the FHIR Provenance resource covers "Generation" of "Entity" with respect to FHIR defined resources for creation or updating; whereas AuditEvent covers "Usage" of "Entity" and all other "Activity" as defined in W3C Provenance. The W3C Provenance Specification has the following fundamental model: Where: Entity - An entity is a physical, digital, conceptual or other kind of thing with some fixed aspects; entities may be real or imaginary. Agent - An agent is something that bears some form of responsibility for an activity taking place, for the existence of an entity, or for another agent's activity. Activity - An activity is something that occurs over a time period and acts upon or with entities. It may include consuming, processing, transforming, modifying, relocating, using, or generating entities. The Provenance resource corresponds to a single activity that identifies a set of resources (target) generated by the activity. The activity also references other entities (entity) that were used and the agents (agent) that were associated with the activity. To record multiple activities that resulted in one (target), record each (activity) in independent Provenance records all pointing at that (target). The Provenance resource depends upon having References to all the resources, entities, and agents involved in the activity. These References need not be resolvable. The references must provide a unique and unambiguous identification. If a resource, entity, or agent can have different versions that must be identified, then the Reference must have versioning information included. Versioning and unique identification are not mandated for all systems that provide Resources, entities, and agents. But, inclusion of Provenance requirements may introduce requirements for versioning and unique identification on those systems The Provenance resource is based on leveraging the W3C Provenance specification to represent HL7 support of provenance throughout its standards and explicitly modeled as functional capabilities in ISO/HL7 10781 EHR System Functional Model Release 2 and ISO 21089 Trusted End-to-End Information Flows. Mappings are provided. The Provenance resource is tailored to fit the FHIR use-cases for provenance more directly. In terms of W3C Provenance the FHIR Provenance resources covers "Generation" of "Entity" with respect to FHIR defined resources for creation or updating; whereas AuditEvent covers "Usage" of "Entity" and all other "Activity" as defined in W3C Provenance. **Referenced By** Contract DeviceRequest MedicationAdministration MedicationDispense MedicationRequest ServiceRequest Task --- ## Questionnaire import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/questionnaire.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A structured set of questions intended to guide the collection of answers from end-users. Questionnaires provide detailed control over order, presentation, phraseology and grouping to allow coherent, consistent data collection. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A Questionnaire is an organized collection of questions intended to solicit information from patients, providers or other individuals involved in the healthcare domain. They may be simple flat lists of questions or can be hierarchically organized in groups and sub-groups, each containing questions. The Questionnaire defines the questions to be asked, how they are ordered and grouped, any intervening instructional text and what the constraints are on the allowed answers. The results of a Questionnaire can be communicated using the QuestionnaireResponse resource. Questionnaires cover the need to communicate data originating from forms used in medical history examinations, research questionnaires and sometimes full clinical specialty records. In many systems this data is collected using user-defined screens and forms. Questionnaires define specifics about data capture - exactly what questions were asked, in what order, what choices for answers were, etc. Each of these questions is part of the Questionnaire, and as such the Questionnaire is a separately identifiable Resource, whereas the individual questions are not. (Questionnaire questions can be linked to shared data elements using the Questionnaire.item.definition element.) Examples of Questionnaires include: Past medical history (PMH) Family diseases Social history Research questionnaires/Clinical research forms (CRFs) Quality and evaluation forms Patient intake form (e.g. clipboard) Forms to support insurance claims This resource is limited in scope to support the characteristics of simple questionnaires. However, common extensions have been defined to allow more sophisticated behavior. This includes: Questionnaire core extensions which defines the additional descriptive characteristics for questionnaires and their groups and questions Element extensions which can describe additional constraints on allowed answers for questionnaires such as string length and date and numeric ranges Additional implementation guides, in particular the Structured Data Capture Questionnaire IG provide guidance, extensions and operations that support additional capabilities and allow the definition of more sophisticated questionnaires and their corresponding responses. **Relationships** Questionnaires differ from Lists because Lists group existing resources, while Questionnaires group arbitrary questions. Questionnaires are distinct from Observations and DiagnosticReports in that both of these resources are intended to capture only certain types of information (lab, imaging, vitals, etc.) and should not be used to capture the full breadth of healthcare information (allergies, medications, care plans, etc.), while Questionnaires are able to capture any information at all. More importantly, Observation and DiagnosticReport focus on capturing the discrete information in a standardized form so that the information can be used consistently regardless of where or how it is captured. Questionnaire focuses on information capture. The same information can be captured using a wide variety of questionnaires with differently phrased questions organized in different manners. As such, the Questionnaire resource provides a means to standardize the information gathering process (how information is captured), but not how data is interoperably compared, analyzed or computed upon (typically managed using Observation, DiagnosticReport as well as other resources). Questionnaire supports data-collection workflow to a limited extent, in that - once triggered - a Questionnaire can guide a user through a data collection process that ensures appropriate information is collected based on answers to particular questions. However, Questionnaire doesn't provide support for capturing sets of information at different times or highly interactive data capture. Broader workflow is typically managed using PlanDefinition and Task or using other mechanisms such as CDSHooks. Questionnaires are similar to the notion of "logical models" supported by the StructureDefinition resource. Both support the representation of a collection of data points with labels, data types and hierarchy. It will be common to find the two of them mapped together. The primary difference is that Questionnaire is focused on user-facing data collection. It describes specific questions and includes information such as what number/label should be displayed beside each question, conditions in which questions should be displayed (or not), what instructions should be provided to the user, etc. StructureDefinition, on the other hand, merely defines a data structure with no guidance on display or rules around capture mechanism, only what data should exist in the end. As well, logical models are not intended to capture data directly. Rather, they provide a basis for mapping between data capture structures. **Background and Context** Groups and questions that make up a Questionnaire can use the definition element to refer to data elements in external code systems and StructureDefinitions. In this way, it's possible to leverage standard definitions of questions and their associated data types and answer sets across questionnaires. This also allows extraction of the data from a questionnaire response into other data structures (e.g. populating a ServiceRequest or CarePlan from information in a completed questionnaire response. It can also support other sorts of post-processing of the data contained in a questionnaire response. However, use of the definition element is not required and Questionnaires may be quite loosely defined. The section Questionnaire versus Resources below discusses the issues of collecting data in such loosely defined Questionnaires versus collecting data as well-defined separate Resources. **Referenced By** CarePlan Communication Contract FamilyMemberHistory PlanDefinition Procedure QuestionnaireResponse --- ## QuestionnaireResponse import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/questionnaireresponse.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A structured set of questions and their answers. The questions are ordered and grouped into coherent subsets, corresponding to the structure of the grouping of the questionnaire being responded to. Read related on [US Core QuestionnaireResponse](http://hl7.org/fhir/us/core/StructureDefinition-us-core-questionnaireresponse.html) profile. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** QuestionnaireResponse provides a complete or partial list of answers to a set of questions filled when responding to a questionnaire. The questions may be included directly or by reference to a Questionnaire resource that defines the questions as well as the constraints on the allowed answers. In some cases, both formal rules for editing the questionnaire (via link to Questionnaire) as well as sufficient local information to allow rendering of the questionnaire may be provided. Each time a questionnaire is completed for a different subject or at a different time, a distinct QuestionnaireResponse is generated, though it may be possible for a previously entered set of answers to be edited or updated. Questionnaire responses cover the need to communicate data originating from forms used in medical history examinations, research questionnaires and sometimes full clinical specialty records. In many systems this data is collected using user-defined screens and forms. Questionnaire responses record specifics about data capture - exactly what questions were asked, in what order, what answers were given, etc. Each of these questions is part of the Questionnaire, and as such the Questionnaire is a separately identifiable Resource, whereas the individual questions are not. Examples of Questionnaires include: Past medical history (PMH) Family diseases Social history Research questionnaires and Case report forms (CRFs) Quality and evaluation forms Patient intake form (e.g. clipboard) Insurance claim form QuestionnaireResponse resources can be validated against their corresponding Questionnaire to verify that required groups and questions are answered and that answers fit constraints in terms of cardinality, data type, etc. **Relationships** The QuestionnaireResponse resource captures the responses to a questionnaire, while Questionnaire represents the definition of the questionnaire form, including what questions are asked, how they're organized and the constraints on the allowed answers. While Observation, with its nested relatedObservation structure, can create complex hierarchies of questions and answers, the focus is different. First, Observation is used primarily for capturing data elements that are "true" observations - lab measurements, vital signs, social assessments, etc., while QuestionnaireResponse can be used to capture any types of data, including data that would typically map to other resources (Procedure, Patient, MedicationStatement, etc.). Second, the focus of QuestionnaireResponse includes the specific phrasing and organization of the questions. All data must be explicitly captured as a question. With Observation, the focus is only on the meaning of the answer, not what question was asked (assuming a question was even asked at all). Additional information such as normal ranges, interpretation, date, etc., may also be captured. **Referenced By** ClinicalImpression Consent Contract DeviceUseStatement FamilyMemberHistory Observation --- ## RelatedPerson import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/relatedperson.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Information about a person that is involved in the care for a patient, but who is not the target of healthcare, nor has a formal responsibility in the care process. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** RelatedPersons typically have a personal or non-healthcare-specific professional relationship to the patient. A RelatedPerson resource is primarily used for attribution of information, since RelatedPersons are often a source of information about the patient. For keeping information about people for contact purposes for a patient, use a Patient's Contact element. Some individuals may serve as both a Patient's Contact and a Related Person. Example RelatedPersons are: A patient's wife or husband A patient's relatives or friends A neighbor bringing a patient to the hospital The owner or trainer of a horse A patient's attorney or guardian A Guide Dog **Relationships** The primary distinction between a Practitioner and a RelatedPerson is based on whether: The person/animal operates on behalf of the care delivery organization over multiple patients (Practitioner) or, Where the person/animal is not associated with the organization, and instead is allocated tasks specifically for the RelatedPerson's Patient (RelatedPerson). A standard extension animalSpecies can be used to indicate the species of a service animal. **Referenced By** Annotation Signature Account AdverseEvent AllergyIntolerance Appointment AppointmentResponse AuditEvent Basic CarePlan CareTeam ChargeItem Claim Communication CommunicationRequest Composition Condition Consent Contract Coverage DeviceRequest DeviceUseStatement DocumentManifest DocumentReference Encounter ExplanationOfBenefit Goal Group ImagingStudy Invoice MeasureReport Media MedicationAdministration MedicationDispense MedicationRequest MedicationStatement Observation Patient Person Procedure Provenance QuestionnaireResponse RequestGroup Schedule ServiceRequest SupplyRequest Task --- ## RequestGroup import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/requestgroup.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A group of related requests that can be used to capture intended activities that have inter-dependencies such as "give this medication after that one". **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is a request resource from a FHIR workflow perspective - see Workflow, specifically Request. The RequestGroup resource is used to represent a group of optional activities that may be performed for a specific patient or context. This resource is often, but not always, the result of applying a specific PlanDefinition to a particular patient. Other than differences that tie the RequestGroup to a particular subject and setting, the actionDefinition element of PlanDefinition is identical to the action element of the RequestGroup, allowing the same features and functionality to be used in both places to describe optionality of and relationships between activities in a workflow. RequestGroups can contain hierarchical groups of actions, where each specific action references the action to be performed (in terms of a Request resource), and each group describes additional behavior, relationships, and applicable conditions between the actions in the overall group. **Referenced By** CarePlan GuidanceResponse --- ## ResearchDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/researchdefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The ResearchDefinition resource describes the conditional state (population and any exposures being compared within the population) and outcome (if specified) that the knowledge (evidence, assertion, recommendation) is about. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is a definition resource from a FHIR workflow perspective - see Workflow, specifically Definition. TODO **Relationships** TODO --- ## ResearchElementDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/researchelementdefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The ResearchElementDefinition resource describes a "PICO" element that knowledge (evidence, assertion, recommendation) is about. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is a definition resource from a FHIR workflow perspective - see Workflow, specifically Definition. TODO **Relationships** TODO **Referenced By** ResearchDefinition --- ## ResearchStudy import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/researchstudy.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A process where a researcher or organization plans and then executes a series of steps intended to increase the field of healthcare-related knowledge. This includes studies of safety, efficacy, comparative effectiveness and other information about medications, devices, therapies and other interventional and investigative techniques. A ResearchStudy involves the gathering of information about human or animal subjects. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is an definition resource from a FHIR workflow perspective - see Workflow. A research study is a scientific way to improve or develop new methods of health care. Studies are designed to answer specific questions on how to prevent, diagnose, or treat diseases and disorders. The ResearchStudy resource describes essential information about the study, including the purpose, objective, sponsor, investigator, therapy, condition being studied, schedule of activities, and other key items. Uses for the ResearchStudy resource include: Registering a clinical trial in trial registry such as clinicaltrials.gov Communicating study protocol information between study stakeholders Supporting study set up of sites, forms Grouping Observation resources by study Setting up a Schedule of Activities for the study and its subjects **Referenced By** UsageContext AdverseEvent MedicinalProduct ResearchSubject --- ## ResearchSubject import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/researchsubject.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A physical entity which is the primary unit of operational and/or administrative interest in a study. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A research subject is the entity of interest in a research study, typically a human being or an animal, but can also be a device, group of humans or animals, or a tissue sample. The ResearchSubject resource describes essential information about the subject, including the gender, birthdate, study, study arm, consent status, and other key items. Human research subjects are usually not traceable to a particular person to protect the subject’s privacy. The ResearchSubject resource is used in the following research activities: Consent and registration to a research study Exposure to test therapy Assessment of test therapy results Reporting of adverse events --- ## RiskAssessment import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/riskassessment.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; An assessment of the likely outcome(s) for a patient or other subject as well as the likelihood of each outcome. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is an event resource from a FHIR workflow perspective - see Workflow, specifically Event. This resource captures predicted outcomes for a patient or population on the basis of source information. Examples include: A prognosis statement for a particular condition Risk of health outcome (heart attack, particular type of cancer) on the basis of lifestyle factors and/or family history List of potential health risks based on a patient's genetic analysis A prediction of outbreak infection rates within a geography based on immunization rates This resource can be used to represent the results of formal scoring/decision support tools that evaluate risk. It can also be used to capture a practitioner's subjective assessment of the patient's risk based on existing knowledge and previous experience. **Relationships** Risk assessments are a specialized type of observation. A specialized resource is used to provide a simpler mechanism to capture of a series of risks and to associate those risks with time-ranges, probabilities, etc. All risk assertions are captured at one time based on a single set of source inputs. Capture of a single risk MAY be done using the Observation class, particularly in circumstances where it's treated as a generic observation, but for consistency, all risk assessments and prognosis SHOULD be captured using RiskAssessment. Risk assessments may be based on a variety of factors, including: Basic demographic information from the Patient or Group resources Various Observations including vital signs, lab information, assessments, genetic information, etc. FamilyMemberHistory Current, past and proposed therapies Immunization, Procedure, CarePlan, etc. Because so many resources can potentially be used, no limit is placed on what resources can be sent in RiskAssessment.basis. However, some resources would be nonsensical for this use (e.g. MessageHeader). AllergyIntolerance and RiskAssessment AllergyIntolerance describes a specific type of risk - propensity to reaction to a substance while RiskAssessment describes general risks to a subject, not generally based on a reaction. **Background and Context** Risk Assessments can be determined manually by health-care providers based on their professional expertise, by using an algorithmic scoring system or through a combination. Risk assessments can also be computed by decision support systems. OperationDefinitions may be defined by servers to generate risk assessments based on particular combinations of source resources or other inputs. **Referenced By** ClinicalImpression Goal --- ## RiskEvidenceSynthesis import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/riskevidencesynthesis.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The RiskEvidenceSynthesis resource describes the likelihood of an outcome in a population plus exposure state where the risk estimate is derived from a combination of research studies. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The scope of the RiskEvidenceSynthesis resource is to describe the likelihood of an outcome in a population with an exposure where the risk estimate is derived from the combination of research studies. Risk estimates are not effects and do not represent a difference between exposure states. Expressing risk estimates is done throughout reporting of biomedical research, systematic reviews, and clinical reference across all disciplines. Risk estimates as a "synthesis" of research studies is a different concept for risk estimation than risk estimates for an individual person. **Referenced By** EffectEvidenceSynthesis --- ## Schedule import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/schedule.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A container for slots of time that may be available for booking appointments. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Schedule resources provide a container for time-slots that can be booked using an appointment. It provides the window of time (period) that slots are defined for and what type of appointments can be booked. The schedule does not provide any information about actual appointments. This separation greatly assists where access to the appointments would not be permitted for security or privacy reasons, while still being able to determine if an appointment might be available. Note: A schedule is not used for the delivery of medication, the Timing data type should be used for that purpose. Context A schedule controls the dates and times available for the performance of a service and/or the use of a resource. One schedule applies to one service or resource, since each service or resource can be reserved independently of the others. If two or more services, people, locations, or things cannot be reserved independently of one another, they are considered to be one activity or resource. A schedule consists of slots of time during which the controlled service or resource is potentially available for provision or use. Slots are categorized as open, booked, or blocked. An open slot on a schedule indicates that the service or resource is available for provision or use during that period of time. A booked slot indicates that the service or resource is not available during the time period, because an appointment has been scheduled. A blocked slot indicates that a service or resource is unavailable for reasons other than a scheduled appointment. The real-world, non-automated analogue of the schedule described above is a standard appointment book. These books are generally organized with rows of time slots, during which a service or resource is available. A slot is one unit on a schedule. A slot represents the smallest unit of time or quantity that a service or resource may be booked. Depending on the nature of the service or resource, there may be more than one defined slot at a given instant of time. For example, if a service is an open group therapy session with twelve available seats, then there are twelve slots for the given block of time. Actor - What the schedule applies to The schedule belongs to a single instance of a service or resource. This is normally a HealthcareService, Practitioner, Location or Device. In the case where a single resource can provide different services, potentially at different location, then the schedulable resource is considered the composite of the actors. For example, if a practitioner can provide services at multiple locations, they might have one schedule per location, where each schedule includes both the practitioner and location actors. When booking an appointment with multiple schedulable resources, multiple schedules may need to be checked depending on the configuration of the system. If an appointment has two practitioners, a specific medical device and a room then there could be a schedule for each of these resources that may need to be consulted to ensure that no collisions occur. If the schedule needed to be consulted, then there would be one created covering the planning horizon for the time of the appointment. Checking availability - Searching When checking availability for an appointment, the creator of the appointment should determine which schedules are applicable, then check for available slots within each schedule. Determining which schedules should be consulted often will involve searching via the properties of the referenced actors, such as the ServiceCategory on the HealthcareService, or the Address on a Location. The type parameter can be used to filter the type of services that can be booked within the associated slots. If all slots are busy, the caller may attempt to book an appointment into an already-booked slot, but the server business rules will dictate whether overbooking is allowed, or whether the appointment may be given a higher precedence and allocated the overbooked slot. **Referenced By** TriggerDefinition Slot --- ## SearchParameter import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/searchparameter.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A search parameter that defines a named search item that can be used to search/filter on a resource. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A SearchParameter resource specifies a search parameter that may be used on the RESTful API to search or filter on a resource. The SearchParameter resource declares: how to refer to the search parameter from a client how the search parameter is to be understood by the server where in the source resource the parameter matches **Relationships** Search Parameters are referred to by CapabilityStatement resources via the canonical URL for a search parameter (CapabilityStatement.rest.resource.searchParam.definition) Search Parameters can appear in an Implementation Guide to specify how resources can be found **Background and Context** Implementers should be familiar with the background and concepts described in Search on the RESTful API before working with this resource. There is a registry of all Search Parameters. **Referenced By** CapabilityStatement --- ## ServiceRequest import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/servicerequest.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A record of a request for service such as diagnostic investigations, treatments, or operations to be performed. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is a request resource from a FHIR workflow perspective - see Workflow. ServiceRequest is a record of a request for a procedure or diagnostic or other service to be planned, proposed, or performed, as distinguished by the ServiceRequest.intent field value, with or on a patient. The procedure will lead to either a Procedure or DiagnosticReport, which in turn may reference one or more Observations, which summarize the performance of the procedures and associated documentation such as observations, images, findings that are relevant to the treatment/management of the subject. This resource may be used to share relevant information required to support a referral or a transfer of care request from one practitioner or organization to another when a patient is required to be referred to another provider for a consultation /second opinion and/or for short term or longer term management of one or more health issues or problems. Examples include: diagnostic tests/studies endoscopic procedures counseling biopsies therapies (e.g., physio-, social-, psychological-) (exploratory) surgeries or procedures exercises specialist consultation and assessments community services nursing services pharmacist medication review, and other clinical interventions. Procedures may be performed by a healthcare professional, a friend or relative or in some cases by the patient themselves. The principal intention of ServiceRequest is to support ordering procedures for one patient (which includes non-human patients in veterinary medicine). However, in many contexts, healthcare related processes include performing diagnostic investigations on groups of subjects, devices involved in the provision of healthcare, and even environmental locations such as ducts, bodies of water, etc. ServiceRequest supports all these usages. The service request may represent an order that is entered by a practitioner in a CPOE system as well as a proposal made by a clinical decision support (CDS) system based on a patient's clinical record and context of care. Planned procedures referenced by a CarePlan may also be represented by this resource. The general work flow that this resource facilitates is that a clinical system creates a service request. The service request is then accessed by or exchanged with a system, perhaps via intermediaries, that represents an organization (e.g., diagnostic or imaging service, surgical team, physical therapy department) that can perform the procedure. The organization receiving the service request will, after it accepts the request, update the request as the work is performed, and then finally issue a report that references the requests that it fulfilled. The ServiceRequest resource allows requesting only a single procedure. If a workflow requires requesting multiple procedures simultaneously, this is done using multiple instances of this resource. These instances can be linked in different ways, depending on the needs of the workflow. For guidance, refer to the Request pattern **Relationships** ServiceRequest is a record of a proposal/plan or order for a service to be performed that would result in a Procedure, Observation, DiagnosticReport, ImagingStudy or similar resource. In contrast to ServiceRequest, Task which spans both intent and event and tracks the execution through to completion and is intended for "administrative" actions like requesting and tracking things to be done to a record, or keeping track of a checklist of steps such to be performed as part of a fulfilment process. A ServiceRequest can be higher-level authorization that triggered the creation of Task, or it can be the "request" resource Task is seeking to fulfill. ServiceRequest and CommunicationRequest are related. A CommunicationRequest is a request to merely disclose information. Whereas a ServiceRequest would be used to request information as part of training or counseling - i.e. when the process will involve verification of the patient's comprehension or an attempt to change the patient's mental state. In some workflows both may exist. For example, upon receiving a CommunicationRequest a practitioner might initiate a ServiceRequest. **Referenced By** Appointment BiologicallyDerivedProduct CarePlan Claim DeviceUseStatement DiagnosticReport Encounter EpisodeOfCare ExplanationOfBenefit Goal ImagingStudy Media MedicationRequest MedicationStatement Observation Procedure QuestionnaireResponse Specimen --- ## Slot import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/slot.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A slot of time on a schedule that may be available for booking appointments. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Slot resources are used to provide time-slots that can be booked using an appointment. They do not provide any information about appointments that are available, just the time, and optionally what the time can be used for. These are effectively spaces of free/busy time. Slots can also be marked as busy without having appointments associated. A slot can have more than one appointment allocated to it. A scheduling system may permit multiple allocations up to a specific number of places. An example of this type of usage could be where the slot is being used for a group service that permits 5 participants at the same time. A slot can be marked as over-booked indicating that there are too many appointments allocated to it. In some situations a service may have a specific set of slots reserved for specific uses, such as "walk-ins" or a specific organization has a "standing booking" for Thursday mornings. These should be represented using the appointmentType field with a specified and agreed value. Security Permissions or specific business rules on the system could enforce that only eligible appointments are allocated to them. If a service had a weekly schedule created that permitted eight 1 hour appointments each day of a working week (Monday - Friday), this would be constructed by a single Schedule resource with the dates for the start and end of the week set, and then 40 (5x8) Slot resources associated with it. As appointments fill up the schedule, these slots would individually be marked as busy as the appointments are filled into the slots. The slots in a schedule do not need to be the same size, and can be different for different days of the week. Slot instances do not have any recurrence information included. If recurring information is desired, this will be managed outside these resources, or included as extensions. Note that booking an appointment does not necessarily require that slot resources be identified. When attempting to book an appointment, if the requestor knows ahead of time which schedulable resources are required, then identifying individual slots from the resources' schedules prior to creating the appointment is appropriate. However, in some medical scheduling scenarios, determining which resources are required for an appointment is very complex, and options other than using schedule+slot may be a better solution. **Referenced By** Appointment --- ## Specimen import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/specimen.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A sample to be used for analysis. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Any material sample: taken from a biological entity, living or dead taken from a physical object or the environment Some specimens are biological and can contain one or more components including but not limited to cellular molecules, cells, tissues, organs, body fluids, embryos, and body excretory products (source: NCI Thesaurus , modified). The specimen resource covers substances used for diagnostic and environmental testing. The focus of the specimen resource is the process for gathering, maintaining and processing the specimen as well as where the specimen originated. This is distinct from the use of Substance which is only used when these other aspects are not relevant. **Background and Context** The current definition of the specimen resource contains only basic information about specimen containers. It does not address the recursive nature of containers or the tracking of the location of a container within its parent container (for instance: a tube in a tray in a rack in a freezer). The frequency with which these elements are tracked may depend on the context of use; general lab, bio-banking, etc. Comments from reviewers on the appropriate scope for this resource, and the need for tracking related specimen management attributes, are welcomed. **Referenced By** DiagnosticReport ImagingStudy Media MolecularSequence Observation ServiceRequest --- ## SpecimenDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/specimendefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A kind of specimen with associated set of requirements. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** SpecimenDefinition will define a kind of specimen, which can be associated with in vitro diagnostic procedures described in a catalog of orderable services. SpecimenDefinition describes the kind of specimen to be collected from the subject of these procedures as well as the requirements applying to the collection activity, the preparation of the subject for this collection. This resource also describes the associated type(s) of specimen conditioned for testing, which are the output of the specimen collection activity. A specimen conditioned for testing is described with the type(s) of container and possible additive to be used, the minimal and normal volumes of collection, the conditions of storage, transportation and handling for the specimen once collected and conditioned. This resource is a necessary building block of a sharable catalog of orderable in vitro diagnostic services. The subjects of these orderable services may be human patients, non-human living subjects or non-living materials such as water, surfaces, medical devices, etc. All sub-specialties of clinical and anatomic pathology laboratories are concerned, as well as all care services prone to order in vitro diagnostic services to those laboratories. A catalog of orderable services generally belongs to a specific laboratory or facility. Nonetheless, the data items used to build this catalog are of universal meaning and interest. **Relationships** This resource relates to these other resources: The SpecimenDefinition resource is referenced by the resource CatalogEntry, when it carries the definition of an in vitro diagnostic testing activity. The SpecimenDefinition resource is referenced by the Catalog resource, which represents a whole catalog, to enable the creation and maintenance of kinds of specimens within a catalog of orderable diagnostic services. The SpecimenDefinition resource references the Substance resource, in order to describe the type of substance (body fluid, other material) concerned by a kind of specimen. It also references the Substance resource to describe additives to be used in the production of specimens of some kind. **Referenced By** ActivityDefinition CatalogEntry --- ## StructureDefinition import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/structuredefinition.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A definition of a FHIR structure. This resource is used to describe the underlying resources, data types defined in FHIR, and also for describing extensions and constraints on resources and data types. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The StructureDefinition resource describes a structure - a set of data element definitions, and their associated rules of usage. These structure definitions are used to describe both the content defined in the FHIR specification itself - Resources, data types, the underlying infrastructural types, and also are used to describe how these structures are used in implementations. This allows the definitions of the structures to be shared and published through repositories of structure definitions, compared with each other, and used as the basis for code, report and UI generation. Note that as part of the specification itself, a full set of structure definitions for all resources and data types is published. **Relationships** StructureDefinitions are used by CapabilityStatement instances for specifying how resources are used StructureDefinitions use Value Sets to specify the content of coded elements **Background and Context** Implementers should be familiar with the background and concepts described in Profiling FHIR before working with this resource. Metadata The StructureDefinition resource has a set of metadata that is mostly shared with the Value Set, CapabilityStatement and other infrastructure resources. The metadata describes the structure and helps find the structure when registered in repositories. url | The identifier that is used to identify this structure when it is referenced in a specification, model, design or an instance. This URL is where the structure can be accessed identifier | Other identifiers that are used to identify this structure version | The identifier that is used to identify this version of the structure when it is referenced in a specification, model, design or instance. This is an arbitrary value managed by the structure author manually and the value should be a timestamp. Note that there may be multiple resource versions of the structure that have this same identifier. The resource will have updates that create new versions for technical reasons, whereas the stated version number needs to be under the author's control name | A Computer-ready name (e.g. a token) that identifies the structure - suitable for code generation. Note that this name (and other names relevant for code generation, including element & slice names, codes etc) may collide with reserved words in the relevant target language, and code generators will need to handle this title | A free text natural language name identifying the structure status | The status of the structure allows filtering of StructureDefinitions that are appropriate for use vs. not. See the Status Codes experimental | This structure was authored for testing purposes (or education/evaluation/marketing), and is not intended for genuine usage date | The date this version of the structure was published publisher | Details of the individual or organization who accepts responsibility for publishing the structure. This helps establish the "authority/credibility" of the structure. contact | Contact details to assist a user in finding and communicating with the publisher description | A free text natural language description of the structure and its use purpose | Why this structure was created - what the intent of it is useContext & Jurisdiction | Computable details about purpose and scope of use keyword | A set of terms from external terminologies that may be used to assist with indexing and searching of StructureDefinitionss copyright | Use and/or publishing restrictions fhirVersion | The version of the FHIR specification on which this structure is based. It is not necessary to specify the version, as most SructureDefinitions are valid across multiple versions, and the validity of a structure against a particular version of FHIR can easily be checked by tooling. type | The type the structure describes. contextType & context | For extensions, the types of contexts in which the extension can be used. For further details, see Defining Extensions Notes: The name and title of the structure are not required to be globally unique, but the name amd title should have some scoping information (e.g. AcmeAllergies / Acme Inc. (USA), Allergy List) Multiple keywords may be assigned to the structure. These may either describe the structure, the focus of the structure or both. They are solely to help find the structure by searching for structured concepts The 3 status codes (draft, active, and retired) are the codes that are relevant to structure consumers. Authors may wish to use the authoring-status extension to track the life cycle of a structure as it is prepared **Referenced By** DataRequirement ElementDefinition Meta ParameterDefinition ActivityDefinition CapabilityStatement GraphDefinition ImplementationGuide MessageDefinition OperationDefinition StructureMap --- ## StructureMap import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/structuremap.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A Map of relationships between 2 structures that can be used to transform data. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The StructureMap resource defines a detailed set of rules that describe how one Structure is related to another and provides sufficient detail to allow for automated conversion of instances. The intention of the structure map resource is to allow a specialist in formats and interoperability to specify the full relationships between two structures (e.g. a CDA document and a set of FHIR resources), and then many different systems - both testing and production clinical systems - can leverage that to automatically transform from one format to the other. Maps are unidirectional: they map from the source structure to the target structure, and no reverse map is implied. Even if the mapping is simple, and loss-less, it cannot be assumed that there are no conditions that might additionally apply in the reverse direction. The mapping language, along with a concrete syntax, is defined in detail in the FHIR Mapping Language. The StructureMap resource represents the abstract syntax, and the concrete syntax is the recommended narrative representation for a StructureMap. See also the Tutorial. **Relationships** Note that many mappings between models only establish conceptual equivalence between the structures. These models are useful because they quickly convey how the structures are related to humans, whereas more maps with sufficient detail to support instance transformation are necessarily full of fine detail that can obscure the conceptual relationships. The ConceptMap resource is suitable for representing high level relationships between models, while this StructureMap resource is intended to describe the full details that need to be known in order to transform an instance of data from one structure to another. The StructureMap resource assumes that both the source and the target models are fully defined using StructureDefinition resources - either resources, or logical models, and is described accordingly. However, there is no direct relationship between the mapping language contained in the StructureMap resource, and the existence of the appropriate structure definitions, so that this mapping language could be used to define a map from an HL7 v2 message to a CDA document. Note, that various implementation contexts may introduce a direct relationship (e.g. see [op to defined]). It's possible to apply the mapping language to structures that do not even have (or cannot have) formally defined types, although the type-related parts of the mapping language cannot be used in these cases. **Background and Context** Each structure map contains, in addition to the standard metadata that all conformance resources contain, the following information: A list of the structure definitions referenced by the map A list of other structure maps that the map uses One or more groups of rules that describe how content in the source is transformed to content in the target Each group of rules defines a set of input and output variables that must be passed when the group is invoked in a particular context. When a group is invoked, all the rules in the group are checked to see whether they apply. Each rule may have some or all of the following properties: A name - used as the identity in internal references, and traces Contexts in both source and target models that define where the rule applies A set of source elements that provide data to be mapped Conditions that specify if the rule is to apply A set of target elements that will be created Transform rules that describe how raw data is converted from the source format to the target format (e.g. string manipulation) Flags for how instances that can repeat are handled Additional rules that apply to the newly created elements (e.g. new contexts) The mapping language is entirely declarative; there is no imperative or procedural aspects to the definitions. **Referenced By** ActivityDefinition PlanDefinition --- ## Subscription import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/subscription.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The subscription resource is used to define a push-based subscription from a server to another system. Once a subscription is registered with the server, the server checks every resource that is created or updated, and if the resource matches the given criteria, it sends a message on the defined "channel" so that another system can take an appropriate action. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** Once a subscription is created, any newly created or updated resources that meet the criteria in the resource cause a notification to be sent using the provided channel. The criteria are Search strings that have the same interpretation as if they were appended to the base URL and submitted using the REST API. Note that the search criteria are applied to the new value of the resource. The consequence of this is that there is no notification when a resource is deleted, or when a resource is updated so that it no longer meets the criteria. The server is able to send notifications without any information about the matching resource, or with the entire resource. Several different types of channels are supported: rest-hook: A post is made to the URL identified in the Subscription resource. If the subscription requests that the whole resource is included, the URL is interpreted as the service base websocket: A PING message is sent to the designated URI email/sms: A notification is sent to the nominated email address or SMS number message: The resource is sent to the application identified in the URI as a message See below for further discussion of the various channels. Note that sending the entire resource creates security concerns that must be managed by the server. Subscriptions are active resources; a server can only accept a subscription if it will execute the specified channel for any resources subsequently received. The subscription is no longer active once it is deleted from the server. **Relationships** As an alternative to subscriptions, the RESTful API describes a polling-based subscription method using bundles and the history operation. This method of polling allows for a much tighter relationship between the client and the server that doesn't involve missing updates and/or deletes. When using the Subscription resource, the FHIR server combines the roles of publisher and information distributer. Other arrangements of the publish and subscribe pattern describe separate agents for the two roles. Implementers may implement the Subscription resource using an architecture with separate agents, or using any other pub/sub architectire (e.g. see FHIRcast , or, more generally, W3C Pub/Sub ). --- ## Substance import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/substance.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A homogeneous material with a definite composition. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource allows for a material to be represented. The resource can be used to represent either a kind of a substance - e.g. a formulation commonly used for treating patients, or it can be used to describe a particular package of a known substance (e.g. bottle, jar, packet). The composition of the material can be specified in terms of a mix of other materials, including with precise amounts if required. **Relationships** A medication is a substance that is packaged and used as an administered medication. The medication resource uses the substance resource to represent the actual ingredients of a medication. **Referenced By** ActivityDefinition AdverseEvent BiologicallyDerivedProduct CarePlan ChargeItem ChargeItemDefinition Contract Group Medication MedicationKnowledge MedicinalProductContraindication MedicinalProductIndication MedicinalProductInteraction Procedure Specimen SpecimenDefinition SubstanceSpecification SupplyDelivery SupplyRequest --- ## SubstanceNucleicAcid import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/substancenucleicacid.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Nucleic acids are defined by three distinct elements: the base, sugar and linkage. Individual substance/moiety IDs will be created for each of these elements. The nucleotide sequence will be always entered in the 5’-3’ direction. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** **Referenced By** SubstanceSpecification --- ## SubstancePolymer import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/substancepolymer.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Todo. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage**
This resource is still undergoing development and review by the appropriate Workgroups. At this time, is considered only as a draft resource not suitable for production implementation
**Referenced By** SubstanceSpecification --- ## SubstanceProtein import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/substanceprotein.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A SubstanceProtein is defined as a single unit of a linear amino acid sequence, or a combination of subunits that are either covalently linked or have a defined invariant stoichiometric relationship. This includes all synthetic, recombinant and purified SubstanceProteins of defined sequence, whether the use is therapeutic or prophylactic. This set of elements will be used to describe albumins, coagulation factors, cytokines, growth factors, peptide/SubstanceProtein hormones, enzymes, toxins, toxoids, recombinant vaccines, and immunomodulators. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** **Referenced By** SubstanceSpecification --- ## SubstanceReferenceInformation import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/substancereferenceinformation.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Todo. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage**
This resource is still undergoing development and review by the appropriate Workgroups. At this time, is considered only as a draft resource not suitable for production implementation
**Referenced By** SubstanceSpecification --- ## SubstanceSourceMaterial import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/substancesourcematerial.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Source material shall capture information on the taxonomic and anatomical origins as well as the fraction of a material that can result in or can be modified to form a substance. This set of data elements shall be used to define polymer substances isolated from biological matrices. Taxonomic and anatomical origins shall be described using a controlled vocabulary as required. This information is captured for naturally derived polymers ( . starch) and structurally diverse substances. For Organisms belonging to the Kingdom Plantae the Substance level defines the fresh material of a single species or infraspecies, the Herbal Drug and the Herbal preparation. For Herbal preparations, the fraction information will be captured at the Substance information level and additional information for herbal extracts will be captured at the Specified Substance Group 1 information level. See for further explanation the Substance Class: Structurally Diverse and the herbal annex. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** **Referenced By** SubstanceSpecification --- ## SubstanceSpecification import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/substancespecification.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; The detailed description of a substance, typically at a level beyond what is used for prescribing. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage**
This resource is still undergoing development and review by the appropriate Workgroups. At this time, is considered only as a draft resource not suitable for production implementation
**Referenced By** MedicinalProductContraindication MedicinalProductIndication --- ## SupplyDelivery import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/supplydelivery.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Record of delivery of what is supplied. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** This resource is an event resource from a FHIR workflow perspective - see Workflow. It is the intent of the Orders and Observation Workgroup to align this resource with the workflow pattern for event resources. The scope of the supply resource is for supplies used in the healthcare process. This includes supplies specifically used in the treatment of patients as well as supply movement within an institution (transport a set of supplies from materials management to a service unit (nurse station). This resource does not include the provisioning of transportation services. **Relationships** This resource overlaps with others such as Device and Medication. The Supply resource may be used to describe medications and devices when handling them generically (as any other supply). For example, when processing bulk orders, etc. However, when the medication, device or other aspects of the resource are important, (e.g. in a MedicationRequest or when identifying a device as a performer), the more detailed resource must be used. **Referenced By** ChargeItem --- ## SupplyRequest import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/supplyrequest.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A record of a request for a medication, substance or device used in the healthcare setting. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage**
Trial-Use Note: In terms of scope and usage, the Orders and Observations workgroups wish to draw the attention of reviewers and implementers to the following issues and changes. We are seeking input from the implementation community on whether to combine SupplyRequest and DeviceRequest for R4. The parameters element was added to SupplyRequest for representing custom details for ordering devices. Feedback is welcome here
This resource is a request resource from a FHIR workflow perspective - see Workflow. It is the intent of the Orders and Observation Workgroup to align this resource with the workflow pattern for request resources. The scope of the SupplyRequest resource is for recording the request of supplies used in the healthcare process. This includes supplies specifically used in the treatment of patients as well as supply movement within an institution (transport a set of supplies from materials management to a service unit (nurse station). This resource does not include the provisioning of transportation services. The SupplyRequest resource allows requesting only a single item. If a workflow requires requesting multiple items simultaneously, this is done using multiple instances of this resource. These instances can be linked in different ways, depending on the needs of the workflow. For guidance, refer to the Request pattern **Relationships** Note that the SupplyRequest records the fact that a request was made. To actually act on that request, additional workflow beyond simply the existence of a SupplyRequest is required. This can be achieved by using an Task resource, with the SupplyRequest referenced from the Task.focus, or by using the SupplyRequest resource in the context of an messaging or service workflow where the request is explicit or implicit. The SupplyDelivery resource represents the fulfillment as a result of SupplyRequest being acted upon. The SupplyRequest resource is used for inventory management. When requesting medication, substances and devices when there is a patient focus or instructions regarding their use, DeviceRequest or MedicationRequest should be used instead **Referenced By** SupplyDelivery --- ## Task import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/task.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A task to be performed. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** A task resource describes an activity that can be performed and tracks the state of completion of that activity. It is a representation that an activity should be or has been initiated, and eventually, represents the successful or unsuccessful completion of that activity. Note that there are a variety of processes associated with making and processing orders. Some orders may be handled immediately by automated systems but most require real world actions by one or more humans. Some orders can only be processed when other real world actions happen, such as a patient presenting themselves so that the action to be performed can actually be performed. Often these real world dependencies are only implicit in the order details. **Relationships** Task spans both intent and event and tracks the execution through to completion. A Task is a workflow step such as cancelling an order, fulfilling an order, signing an order, merging a set of records, admitting a patient. In contrast, Procedures are actions that are intended to result in a physical or mental change to or for the subject (for example, surgery, physiotherapy, training, counseling). A Task resource often exists in parallel with clinical resources. For example, a Task could request fulfillment of a ServiceRequest ordering a Procedure that would result in a Procedure, Observation, DiagnosticReport, ImagingStudy or similar resource. Another example would be a Task that requests fulfillment of a CommunicationRequest to be performed between various actors. As stated above, the task resource tracks the state of a task, enabling systems to ensure that tasks are completed. This information is kept separate from the operational details necessary to complete the task, as those details vary across and even within workflows. That detail is expected to be carried in the referenced `focus` of the task. Tasks may have named inputs and outputs. Inputs represent information that may or must be present in order for a task to complete. Outputs represent intermediate or final results produced by a task. For example, in a task supporting reading of radiology image, the inputs might include both the imaging study to be read, as well as relevant prior images. Outputs could represent radiology measurements as well as the Radiologist's diagnostic report. Inputs and outputs are tracked by the task because workflow management activity may automate the transfer of outputs from one task to inputs to a subsequent task. To facilitate the integration of off the shelf workflow applications with FHIR, the task resource may reference a definition. This definition can represent a description of the workflow activity to be performed, using a standard workflow description language such as BPEL, BPMN, or XPDL, a workflow definition such as those defined in IHE profiles, or even simple written instructions explaining a process to be performed. **Background and Context** Using Tasks in a RESTful context In a RESTful context, a server functions as a repository of tasks. The server itself, or other agents are expected to monitor task activity and initiate appropriate actions to ensure task completion, updating the status of the task as it proceeds through its various stages of completion. These agents can be coordinated, following well choreographed business logic to ensure that tasks are completed. Task management may also be centrally controlled using some form of a workflow engine, in which case, the workflow engine itself may update and maintain the task resources in the server, and through its orchestration activities ensure that tasks are completed. The task resource enables either model of task management yet provides a consistent view of the status of tasks being executed in support of healthcare workflows. The assignment of tasks into categories by type of task, type of performer and task status enable the server to function as a queue of work items. This queue can be polled or subscribed to by various agents, enabling automation of workflows in FHIR using existing search and subscription mechanisms. Owners, requesters, other agents (e.g. workflow managers) can thus be ready to initiate the next steps in a complex workflow. Tasks State Machine Tasks start in a Created state. Once they have been assigned to an owner they transition to the Ready state, indicating that they are ready to be performed. Once the owner initiates activity on the task, the task transitions to the In Progress state, indicating that work is being performed. Upon normal completion, the task enters the Completed state. If there is a failure during the task execution that prevents the task from being completed, it can also enter a Failed state, indicating an abnormal termination of the task. A task in any non-terminal state may also be Cancelled, representing an abnormal termination of the task due to external forces, rather than an error condition. Tasks in any non-terminal state (Created, Ready, In Progress) can be suspended and resumed. When a task is suspended, it is typically resumed in the state it was in when it was originally suspended. Suspending a task suspends all of its children as well. Resuming a task resumes all of its children. An In-progress task can also be stopped, returning it to a Ready state. This may be in preparation for delegation or reassignment (e.g., because it cannot be completed by the current owner), to restart a task due to a temporary failure (e.g., to reattempt completion of the activity), or in preparation to allow others to claim the task. The task history allows applications monitoring the state of a workflow to identify tasks that are long running, perhaps stuck in some queue, to enable management activities that could ensure completion. It also enables tracking of task statistics such as wait time, or time in progress, or time to completion, enabling capture of important task metrics in support of optimization and quality improvement.
Note: Currently, task pre-requisites can be represented using Task's `description` element or the RequestGroup resource. We are seeking input from the implementer community in evaluating whether there is need for adding another mechanism to this resource for this purpose. Feedback here .
**Referenced By** CarePlan ImagingStudy PaymentReconciliation --- ## TerminologyCapabilities import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/terminologycapabilities.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A TerminologyCapabilities resource documents a set of capabilities (behaviors) of a FHIR Terminology Server that may be used as a statement of actual server functionality or a statement of required or desired server implementation. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The TerminologyCapabilities resource provides for a terminology server to describe how the underlying terminology service works. A client can get the TerminologyCapabilities statement for a Terminology Service by using the capabilities API:
 GET [base]/metadata?mode=terminology 
This will return the TerminologyCapabilities resource that applies to the service itself. **Relationships** The TerminologyCapabilities resource does not describe the API - see CapabilityStatement for this. The CodeSystem resource documents the inherent structure and capabilities of code system, whereas the TerminologyCapabilities resource documents what a server hosting the code system is capable of --- ## TestReport import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/testreport.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A summary of information based on the results of executing a TestScript. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The TestScript resource is used to define tests that can be executed on one or more FHIR servers. The TestReport resource defines how systems should encode the summarized results of executing a TestScript. The TestReport structure mirrors the TestScript concepts of having sections for setup, tests, and teardown. If present, these ordered lists should mirror the actions (either operations or assertions) of the referenced TestScript with a result code: pass, skip, fail, warning, or error. **Background and Context** Background Implementers should be familiar with the testing concepts and descriptions found in Testing FHIR before working with this resource. --- ## TestScript import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/testscript.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A structured set of tests against a FHIR server or client implementation to determine compliance against the FHIR specification. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The TestScript resource is used to define tests that can be executed on one or more FHIR servers. The TestScript resource would typically contain a list of fixtures (required resources used in the tests) setup procedures a suite of thematically related tests teardown procedures For example, one TestScript might feature a set of tests focusing on searching Patients and validating the Bundle responses. The fixtures for such a test would contain a list of Patient resources that are required for the test to complete successfully. The setup procedures create the fixtures on the FHIR server being tested. A series of tests execute various search parameters and search for the fixtures in the results. The teardown procedures would then clean up (delete) the fixtures on FHIR server that were created during the setup procedures. The purpose of the TestScript is to encode in an executable representation tests that can be used to determine whether a given FHIR server adheres to the FHIR specification and determine whether two or more FHIR servers implement capabilities in a compatible or interoperable manner. It might not be possible to fully automate the latter goal (especially with regards to business rules that might ride on top of different implementations), however the tests should be able to determine whether two servers support the operations, value sets, profiles, and extensions to exchange Bundles of Resources (such as returned from the Patient $everything operation). **Relationships** The TestScript resource should NOT be used to represent Clinical tests, Prescriptions, or any other Healthcare related concept. The TestScript resource is an infrastructure support resource used to represent standardized tests to determine an implementation's level of adherence to the FHIR specification. TestScript is a part of the conformance framework and is used to validate the behavior of FHIR systems, specifically their correct implementation of StructureDefinition, ValueSet, OperationDefinition, CapabilityStatement and other FHIR resources that govern system behavior. TestScript instances may be included as part of ImplementationGuides to help define and test the desired behavior of systems that choose to comply with the implementation guide. The following resources represent the FHIR conformance framework that are used to express the expected behavior of a FHIR compliant system: ValueSet ConceptMap StructureDefinition CapabilityStatement OperationDefinition SearchParameter ImplementationGuide **Background and Context** Background The TestScript resource is designed to establish testing as a first class artifact within the FHIR specification. This resource allows defining a suite of tests that can be executed on one or more FHIR servers and clients. Implementers should be familiar with the testing concepts and descriptions found in Testing FHIR before working with this resource. **Referenced By** TestReport --- ## ValueSet import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/valueset.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; A ValueSet resource instance specifies a set of codes drawn from one or more code systems, intended for use in a particular context. Value sets link between [CodeSystem](./codesystem) definitions and their use in [coded elements](https://www.hl7.org/fhir/terminologies.html). **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The FHIR terminology specification is based on two key concepts, originally defined in HL7 v3 Core Principles : CodeSystem - declares the existence of and describes a code system or code system supplement and its key properties, and optionally defines a part or all of its content. Also known as Ontology, Terminology, or Enumeration ValueSet - specifies a set of codes drawn from one or more code systems, intended for use in a particular context. Value sets link between CodeSystem definitions and their use in coded elements Value sets have 2 aspects: .compose: A definition of which codes are intended to be in the value set ("intension") .expansion: The list of codes that are actually in the value set under a given set of conditions ("extension") - see Value Set Expansion The ValueSet resource can carry either the .compose or the .expansion, both of them, or neither of them (if only the metadata is being represented). There is an "$expand" operation which can be used to ask a server to generate an expansion given the composition rules, in a particular context, and a "$validate-code" operation which can be used to ask a server to check whether a given code or concept is in the value set in a particular context. **Relationships** Value Sets are used by many resources: Value sets use CodeSystem resources by referring to them via their canonical reference. Value sets are used in StructureDefinition, OperationDefinition, Questionnaire, and other resources to specify the allowable contents for coded elements, or business rules for data processing ConceptMap resources describe mappings between value sets DataRequirement data types to specify data processing conditions For a full list of uses, see below. The Characteristics of the ValueSet resource are derived from Formal Value Set Definitions: The ValueSet resource design is based on the functionality described in the OMG CTS 2 specification, along with metadata in the HL7 Value Set Definition specification. Value set resources can be converted to CTS2 value set or code system instances. The value set resource is aligned with the Value Set Definition (VSD) project. Not all of the elements defined by the VSD are part of the base resource - some are defined as part of the ValueSet Extensions. In the ValueSet resource, the compose element is the VSD "Content Logical definition". **Background and Context** When using value sets, proper differentiation between a code system and a value set is important. This is one very common area where significant clinical safety risks occur in practice. Implementers should be familiar with the content in Using Codes in Resources. ValueSet Identification A value set has 3 identifiers: ValueSet.id: the logical id on the system that holds the value set - this changes as it moves from server to server (this id, with the server address prepended, is called the 'literal identity' of the resource) ValueSet.url: the canonical URL that never changes for this value set - it is the same in every copy. The element is named url rather than uri for legacy reasons and to strongly encourage providing a resolvable URL as the identifier whenever possible. Ideally, it should be a literal URL that is the location of the master version of the value set, though this is not always possible ValueSet.identifier: A system/value pair that is used to identify the value set in other contexts (such as an OID in an HL7 v3 specification) In addition, any expansion for the value set also has ValueSet.expansion.identifier which uniquely identifies each expansion. For further information regarding resource identification, see Resource Identity. This means that each value set has 2 different URLs that can be used to reference it - its canonical URL (the url element), and its local location from which it may be retrieved (which includes the id element). Because it is common practice to copy (cache) value sets locally, most references to value sets use the canonical URL.
For example, the value sets published as part of FHIR all have a location ("literal") URI which is the URL where they may be accessed in the FHIR specification itself. Note, though, that while a new version of the FHIR Specification is being prepared, value sets that are published in the drafts will not be found in the current published FHIR specification at their canonical URL.
Alternatively, the identifier and version elements may be used to reference this value set in a design, a profile, a CDA template or HL7 v3 message (in the CD data type valueSet and valueSetVersion properties). These different contexts may make additional restrictions on the possible values of these elements. The identifier is generally not needed when using value sets in a FHIR context, where the canonical URL is always the focus. Intensional vs Extensional A value set may be described as intensional or extensional. The terms intensional and extensional come from the fields of mathematical logic and set theory. An intensional value set is typically algorithmically defined. That is, the code group is defined as a rule e.g. all codes with the word diabetes in their description). The key benefit of intensional code groups is that they can be dynamically updated. Dynamic updating helps healthcare organizations keep current when new drugs (and their associated codes) become available or codes for diseases and other clinical concepts change. An intensional value set designed to contain all of the drugs in the beta blocker category can automatically receive a new beta blocker’s code as soon as it hits the market. Extensional value sets, meanwhile, are enumerated lists of codes where each code is listed individually. This gives the author and user of the value set more control over the which codes are in the value set, but there is a greater maintenance burden to ensure that the value set is kept up to date. **Referenced By** DataRequirement ElementDefinition CodeSystem ConceptMap ObservationDefinition OperationDefinition Questionnaire ResearchElementDefinition --- ## VerificationResult import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/verificationresult.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; Describes validation requirements, source(s), status and dates for one or more elements. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The VerificationResult can be used where content (such as found in a directory) is aggregated between systems, and the details of this process needs to be recorded, to be able to determine the likely accuracy of the content. **Relationships** TBD. --- ## VisionPrescription import Link from '@docusaurus/Link'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import definition from '@site/static/data/resourceDefinitions/visionprescription.json'; import { ResourcePropertiesTable, SearchParamsTable } from '@site/src/components/ResourceTables'; An authorization for the provision of glasses and/or contact lenses to a patient. **Schema** ### Elements ### Search Parameters ### Inherited Elements **Usage** The VisionPrescription resource is intended to support the information requirements for a prescription for glasses and contact lenses for a patient. Corrective optical lenses are considered a controlled substance and therefore a prescription is typically required for the provision of patient-specific lenses. Vision prescriptions may only be issued by providers who have optical diagnosis within their scope of practice, typically ophthalmologists and optometrists. The prescription is a time-limited, typically two years, authorization for the provision of an unlimited number of lenses, glasses or contacts, which conform to the authorized specification by providers which have lens provision within their scope of practice, typically ophthalmologists , optometrists and opticians. Health insurers typically require a copy of the prescription to ensure that a vision professional has examined the patient and that the power of the lenses exceeds some minimum, such as +- 0.75 diopters, to qualify for reimbursement under the insurance coverage. The VisionPrescription resource is an "request" resource from a FHIR workflow perspective - see Workflow Request. Additional Information Additional information regarding resources conveying electronic claims content and usage may be found at: Financial Resource Status Lifecycle: how .status is used in the financial resources. **Relationships** The VisionPrescription resource is used to provide the authorized specification of lenses for glasses and contacts which have well known and internationally consistent requirements, which is distinct from other types of health care authorizations detailed below. The health care domain includes a number of related resources VisionPrescription | The VisionPrescription resource is used to provide the authorized specification of lenses for glasses and contacts. DeviceRequest | This resource is used to prescribe, authorize, and request provision of a health care device such as a halter monitor or a wheelchair services. MedicationRequest | This resource is used to prescribe, authorize, and request provision of medications such a pharmaceuticals and vaccines. ServiceRequest | This resource is used to prescribe, authorize, and request provision of health care services such as consultative or rehabilitative services. **Referenced By** CarePlan Claim ExplanationOfBenefit --- ## API Overview Welcome to the Medplum API reference. This section is intended as reference material, overview and how-to guides can be found in the [Documentation](/docs) section. The base URL for hosted Medplum is https://api.medplum.com/fhir/R4. ### System Requirements The API requires [registration](/docs/tutorials/register) and acceptance of the [terms of service](/terms). The following standards are supported: - US Core 5.0.1 / USCDI v2 - SMART App Launch 2.0.0 - Bulk Data 2.0.0 Apps using the API must be able to: - Support OAuth 2.0 or SMART-on-FHIR authentication framework - Connect via HTTPS with TLS 1.2 and 1.3 - Store the client_id and client_secret for authentication - Process JSON response files ### Authentication and Authorization If you are using Medplum as an identity-provider, [**OAuth**](./api/oauth) documents the endpoints supplied by the Medplum server to complete the [OAuth2 Auth Code Flow](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow). Medplum supports a wide variety of authentication and authorization options, including SMART-on-FHIR scopes. #### SMART-on-FHIR Medplum uses the SMART Application Launch Framework Implementation Guide (http://hl7.org/fhir/smart-app-launch/1.0.0/) and provides an OAuth 2.0 compliant authorization server to enable patients and providers secure authentication access to their health data. As described by the specification itself, SMART on FHIR: “Connects third-party applications to Electronic Health Record data, allowing apps to launch from inside or outside the user interface of an EHR system. The framework supports apps for use by clinicians, patients, and others via PHR or Patient Portal or any FHIR system where a user can give permissions to launch an app. It provides a reliable, secure authorization protocol for a variety of app architectures, including apps that run on an end-user’s device as well as apps that run on a secure server.” #### Patient User Access Through their selected application, patients will be prompted for their login and credentials which they established in their patient portal accounts. SMART-on-FHIR enables the user to authenticate themselves with Medplum authorization and choose the scope of their patient data which they intend to access and share with their application. This decision provides an access token to their application of choice, and applications which are compliant with SMART on FHIR can then use this access token to access their health data stored on Medplum. These applications will continue to have access to the patient data until the patient revokes their access permission. Related material: - [Patient](/docs/api/fhir/resources/patient) FHIR resource - [User](/docs/api/fhir/medplum/user) - [Project Membership](/docs/api/fhir/medplum/projectmembership) - [Access Control](/docs/access/access-policies) documentation #### Clinician User Access Clinician users of Medplum can also use approved SMART-on-FHIR applications to access patient data in the system. Clinicians need to be explicitly granted Practitioner accounts with the appropriate [access controls](/docs/access/access-policies) in order to consume patient data. They will need to be [invited](/docs/app/invite) by administrator. ### FHIR Resources For documentation on all the FHIR Resources that Medplum supports, check out our [**FHIR API docs**](./api/fhir). If you're asking yourself, "_What's FHIR?_", check out our [FHIR Basics page](/docs/fhir-basics). The full list of [FHIR resources](/docs/api/fhir/resources) page describes the resources and [data types](/docs/api/fhir/datatypes) in detail. This API reference is an Open Source implementation of the [ONC Certification (g)(10) Criteria](/docs/compliance/onc) We also make available our [Open API Spec](https://api.medplum.com/openapi.json). ### Error Handling The following resources can be used to understand error messages and error handling workflows. Per the FHIR spec errors are [OperationOutcome](/docs/api/fhir/resources/operationoutcome) resources with [type](https://hl7.org/fhir/valueset-issue-type.html) and [severity](https://hl7.org/fhir/valueset-issue-severity.html) properties. In case of server errors you can expect [HTTP Status codes](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes). ### SDK Our [**TypeScript SDK**](./sdk/core.medplumclient) provides a powerful, type-safe library to create and store healthcare data in Medplum. The SDK can be used in standalone applications and is used by [bots](/docs/bots). ### Prebuilt UI If you're building a [**React**](https://reactjs.org/) app with Medplum, our [**React Components**](./api/react) can give you building blocks to get you started. --- ## Authorize endpoint The `/oauth2/authorize` endpoint signs in the user. ### GET `/oauth2/authorize` The `/oauth2/authorize` endpoint only supports `HTTPS GET`. The client typically makes this request through a browser. The authorization server requires HTTPS. For more information about the OpenID Connect specification, see [Authorization Endpoint](http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthorizationEndpoint). ### Request parameters ##### `response_type` (Required) The response type. Must be `code`. ##### `client_id` (Required) The Client ID. Must be a client that you already registered. . ##### `redirect_uri` (Required) The URL where the authentication server redirects the browser after Medplum authorizes the user. A redirect uniform resource identifier (URI) must have the following attributes: - It must be an absolute URI. - You must have pre-registered the URI with a client. - The `redirect_uri` parameter must **match exactly** to the pre-registered uri on the Medplum Server (including trailing slashes, punctuation, etc.), as detailed in the [OAuth2 Spec](https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2.3) - It can't include a fragment component. See [OAuth 2.0 - Redirection Endpoint](https://tools.ietf.org/html/rfc6749#section-3.1.2). Medplum requires that your redirect URI use HTTPS, except for `http://localhost`, which you can set as a callback URL for testing purposes. ##### `state` (Optional but recommended) When your app adds a _state_ parameter to a request, Medplum returns its value to your app when the `/oauth2/authorize` endpoint redirects your user. Add this value to your requests to guard against [CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery) attacks. You can't set the value of a `state` parameter to a URL-encoded JSON string. To pass a string that matches this format in a `state` parameter, encode the string to Base64, then decode it in your app. ##### `scope` (Required) Can be a combination of any system-reserved scopes or custom scopes that are associated with a client. Scopes must be separated by spaces. System reserved scopes are `openid`, `email`, `phone`, and `profile`. If the client doesn't request any scopes, the authentication server uses `openid` by default. ##### `code_challenge_method` (Optional) The method that you used to generate the challenge. The [PKCE RFC](https://tools.ietf.org/html/rfc7636) defines two methods, S256 and plain. ##### `code_challenge` (Conditional) The challenge that you generated from the `code_verifier`. Required only when you specify a `code_challenge_method` parameter. ##### `nonce` A random value that you can add to the request. The nonce value that you provide is included in the ID token that Medplum issues. You can use a `nonce` value to guard against replay attacks. ##### `prompt` (Optional) Controls how the authorization server prompts the user for consent. Supported values: - `none`: The authorization server must not display any authentication or consent user interface pages. If the user is not already authenticated, or consent is required but has not been previously granted, the authorization server will return an error. This is particularly useful in SMART App Launch scenarios where you want to ensure a seamless experience when the user is already authenticated. - `login`: The authorization server should prompt the user for reauthentication, even if they're already authenticated. If this parameter is not specified, the authorization server will prompt the user for authentication if they're not already logged in, and for consent if required. ### Examples requests with positive responses #### Authorization code grant ##### Sample Request ``` GET https://api.medplum.com/oauth2/authorize? response_type=code& client_id=492e4ec3-fb66-4b45-b529-599c708ec530& redirect_uri=https://YOUR_APP/redirect_uri& state=STATE& scope=openid ``` ##### Sample response The Medplum authentication server redirects back to your app with the authorization code and state. The code and state must be returned in the query string parameters and not in the fragment. A query string is the part of a web request that appears after a '?' character; the string can contain one or more parameters separated by '&' characters. A fragment is the part of a web request that appears after a '#' character to specify a subsection of a document. **Note** The response returns a one time use code that is valid for five minutes. ``` HTTP/1.1 302 Found Location: https://YOUR_APP/redirect_uri?code=AUTHORIZATION_CODE&state=STATE ``` #### Authorization code grant with PKCE ##### Sample Request ``` GET https://api.medplum.com/oauth2/authorize? response_type=code& client_id=492e4ec3-fb66-4b45-b529-599c708ec530& redirect_uri=https://YOUR_APP/redirect_uri& state=STATE& scope=openid& code_challenge_method=S256& code_challenge=CODE_CHALLENGE ``` ##### Sample Response The authentication server redirects back to your app with the authorization code and state. The code and state must be returned in the query string parameters and not in the fragment. ``` HTTP/1.1 302 Found Location: https://YOUR_APP/redirect_uri?code=AUTHORIZATION_CODE&state=STATE ``` #### Token grant without `openid` scope ##### Sample Request ``` GET https://api.medplum.com/oauth2/authorize? response_type=token& client_id=492e4ec3-fb66-4b45-b529-599c708ec530& redirect_uri=https://YOUR_APP/redirect_uri& state=STATE& scope=profile ``` ##### Sample Response The Medplum authorization server redirects back to your app with access token. Because `openid` scope was not requested, Medplum doesn't return an ID token. Also, Medplum doesn't return a refresh token in this flow. Medplum returns the access token and state in the fragment and not in the query string. ``` HTTP/1.1 302 Found Location: https://YOUR_APP/redirect_uri#access_token=ACCESS_TOKEN&token_type=bearer&expires_in=3600&state=STATE ``` #### Token grant with `openid` scope ##### Sample Request ``` GET https://api.medplum.com/oauth2/authorize? response_type=token& client_id=492e4ec3-fb66-4b45-b529-599c708ec530& redirect_uri=https://YOUR_APP/redirect_uri& state=STATE& scope=openid ``` ##### Sample Response The authorization server redirects back to your app with access token and ID token (because `openid` scope was included). ``` HTTP/1.1 302 Found Location: https://YOUR_APP/redirect_uri#id_token=ID_TOKEN&access_token=ACCESS_TOKEN&token_type=bearer&expires_in=3600&state=STATE ``` ### Examples of negative responses The following are examples of negative responses: ###### `invalid_request` If `client_id` and `redirect_uri` are valid, but the request parameters aren't formatted correctly, the authentication server redirects the error to client's `redirect_uri` and appends an error message in a URL parameter. Examples of incorrect formatting are a request doesn't include a `response_type` parameter, if the response provides `code_challenge` but not `code_challenge_method`, or that `code_challenge_method` is not 'S256'. ``` HTTP 1.1 302 Found Location: https://client_redirect_uri?error=invalid_request ``` ###### `unauthorized_client` If the client requests `code` or `token` in `response_type` but doesn't have permission for these requests, the Medplum authorization server returns `unauthorized_client` to client's `redirect_uri`, as follows: ``` HTTP 1.1 302 Found Location: https://client_redirect_uri?error=unauthorized_client ``` ###### `invalid_scope` If the client requests scope that is unknown, malformed, or not valid, the Medplum authorization server returns `invalid_scope` to the client's `redirect_uri`, as follows: ``` HTTP 1.1 302 Found Location: https://client_redirect_uri?error=invalid_scope ``` ###### If there is any unexpected error in the server, the authentication server returns `server_error` to client's `redirect_uri`. Because the HTTP 500 error doesn't get sent to the client, don't display the error to the user in the browser. The following error should result: ``` HTTP 1.1 302 Found Location: https://client_redirect_uri?error=server_error ``` ###### timeout When Medplum authenticates through federation to third-party IdPs, Medplum might experience connection issues such as the following: - If a connection timeout occurs while requesting token from the IdP, the authentication server redirects the error to the client's `redirect_uri` as follows: ``` HTTP 1.1 302 Found Location: https://client_redirect_uri?error=invalid_request& error_description=Timeout+occurred+in+calling+IdP+token+endpoint ``` - If a connection timeout occurs while calling the `jwks` endpoint for `id_token` validation, the authentication server redirects the error to the client's `redirect_uri` as follows: ``` HTTP 1.1 302 Found Location: https://client_redirect_uri?error=invalid_request& error_description=error_description=Timeout+in+calling+jwks+uri ``` ###### configuration error - When authenticating by federating to third-party IdPs, the providers may return error responses due to configuration errors or otherwise such as the following: - If an error response is received from other providers, the authentication server redirects the error to the client's `redirect_uri` as follows: ``` HTTP 1.1 302 Found Location: https://client_redirect_uri?error=invalid_request& error_description=[IdP name]+Error+-+[status code]+error getting token ``` - If an error response is received from Google, the authentication server redirects the error to the client's `redirect_uri` as follows: ``` HTTP 1.1 302 Found Location: https://client_redirect_uri?error=invalid_request& error_description=Google+Error+-+[status code]+[Google provided error code] ``` ###### connection error - When Medplum encounters an exception in the communication protocol while it makes a connection to an external IdP, the authentication server redirects the error to the client's `redirect_uri` with either of the following messages: ``` HTTP 1.1 302 Found Location: https://client_redirect_uri?error=invalid_request& error_description=Connection+reset ``` ``` HTTP 1.1 302 Found Location: https://client_redirect_uri?error=invalid_request& error_description=Read+timed+out ``` --- ## Token endpoint The `/oauth2/token` endpoint gets the user's tokens. ### POST `/oauth2/token` The `/oauth2/token` endpoint only supports `HTTPS POST`. Your app makes requests to this endpoint directly, not through the user's browser. For more information about the token endpoint from the OpenID Connect specification, see [Token Endpoint](http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). ### Request parameters in header ##### `Authorization` If the client was issued a secret, the client must pass its `client_id` and `client_secret` in the authorization header through Basic HTTP authorization. The authorization header string is [Basic](https://en.wikipedia.org/wiki/Basic_access_authentication#Client_side) `Base64Encode(client_id:client_secret)`. The following example is an authorization header for app client `492e4ec3-fb66-4b45-b529-599c708ec530&` with client secret `abcdef01234567890`, using the Base64-encoded version of the string `492e4ec3-fb66-4b45-b529-599c708ec530&:abcdef01234567890`. ``` Authorization: Basic ZGpjOTh1M2ppZWRtaTI4M2V1OTI4OmFiY2RlZjAxMjM0NTY3ODkw ``` ##### `Content-Type` Must always be `'application/x-www-form-urlencoded'`. ### Request parameters in body ##### `grant_type` (Required) Grant type. Must be `authorization_code` or `refresh_token` or `client_credentials`. You can request an access token for a custom scope from the token endpoint when, in the app client, the requested scope is enabled, you have configured a client secret, and you have allowed `client_credentials` grants. ##### `client_id` (Required) The Client ID. Must be a client that you already registered. ##### `scope` (Optional) Can be a combination of any custom scopes associated with an app client. Any scope that you request must be activated for the app client, or Medplum will ignore it. If the client doesn't request any scopes, the authentication server uses all custom scopes associated with the client. Only used if the `grant_type` is `client_credentials`. ##### `redirect_uri` (Conditional) Must be the same `redirect_uri` that was used to get `authorization_code` in `/oauth2/authorize`. Required only if `grant_type` is `authorization_code`. ##### `refresh_token` (Optional) The refresh token. The token endpoint returns `refresh_token` only when the `grant_type` is `authorization_code`. ##### `code` (Conditional) Required if `grant_type` is `authorization_code`. ##### `code_verifier` (Conditional) The proof key. Required if `grant_type` is `authorization_code` and the authorization code was requested with PKCE. ### Examples requests with positive responses #### Exchanging an authorization code for tokens ##### Sample Request ``` POST https://api.medplum.com/oauth2/token& Content-Type='application/x-www-form-urlencoded'& Authorization=Basic aSdxd892iujendek328uedj grant_type=authorization_code& client_id=492e4ec3-fb66-4b45-b529-599c708ec530&& code=AUTHORIZATION_CODE& redirect_uri=https://myclient/redirect ``` ##### Sample Response ``` HTTP/1.1 200 OK Content-Type: application/json { "access_token":"eyJz9sdfsdfsdfsd", "id_token":"dmcxd329ujdmkemkd349r", "token_type":"Bearer", "expires_in":3600 } ``` **Note** The token endpoint returns `refresh_token` only when the `grant_type` is `authorization_code` and the requested scopes during login included `offline` or `offline_access`. #### Exchanging client credentials for an access token ##### Sample Request ``` POST https://api.medplum.com/oauth2/token Content-Type='application/x-www-form-urlencoded'& Authorization=Basic aSdxd892iujendek328uedj grant_type=client_credentials& scope=openid ``` ##### Sample Response ``` HTTP/1.1 200 OK Content-Type: application/json { "access_token":"eyJz9sdfsdfsdfsd", "token_type":"Bearer", "expires_in":3600 } ``` #### Exchanging an authorization code grant with PKCE for tokens ##### Sample Request ``` POST https://api.medplum.com/oauth2/token Content-Type='application/x-www-form-urlencoded'& Authorization=Basic aSdxd892iujendek328uedj grant_type=authorization_code& client_id=492e4ec3-fb66-4b45-b529-599c708ec530&& code=AUTHORIZATION_CODE& code_verifier=CODE_VERIFIER& redirect_uri=https://myclient/redirect ``` ##### Sample Response ``` HTTP/1.1 200 OK Content-Type: application/json { "access_token":"eyJz9sdfsdfsdfsd", "refresh_token":"dn43ud8uj32nk2je", "id_token":"dmcxd329ujdmkemkd349r", "token_type":"Bearer", "expires_in":3600 } ``` **Note** The token endpoint returns `refresh_token` only when the `grant_type` is `authorization_code`. #### Exchanging a refresh token for tokens ##### Sample Request ``` POST https://api.medplum.com/oauth2/token > Content-Type='application/x-www-form-urlencoded'& Authorization=Basic aSdxd892iujendek328uedj grant_type=refresh_token& client_id=492e4ec3-fb66-4b45-b529-599c708ec530&& refresh_token=REFRESH_TOKEN ``` ##### Sample Response ``` HTTP/1.1 200 OK Content-Type: application/json { "id_token":"eyJz9sdfsdfsdfsd", "access_token":"dmcxd329ujdmkemkd349r", "token_type":"Bearer", "expires_in":3600 } ``` :::info[Note] The token endpoint returns `refresh_token` only when the `grant_type` is `authorization_code`. ::: ### Examples of negative responses ##### Sample Error Response ``` HTTP/1.1 400 Bad Request Content-Type: application/json;charset=UTF-8 { "error":"invalid_request|invalid_client|invalid_grant|unauthorized_client|unsupported_grant_type|" } ``` ###### `invalid_request` The request is missing a required parameter, includes an unsupported parameter value (other than `unsupported_grant_type`), or is otherwise malformed. For example, `grant_type` is `refresh_token` but `refresh_token` is not included. ###### `invalid_client` Client authentication failed. For example, when the client includes `client_id` and `client_secret` in the authorization header, but there's no such client with that `client_id` and `client_secret`. ###### `invalid_grant` Refresh token has been revoked. Authorization code has been consumed already or does not exist. App client doesn't have read access to all [attributes](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html) in the requested scope. For example, your app requests the `email` scope and your app client can read the `email` attribute, but not `email_verified`. ###### `unauthorized_client` Client is not allowed for code grant flow or for refreshing tokens. ###### `unsupported_grant_type` Returned if `grant_type` is anything other than `authorization_code` or `refresh_token` or `client_credentials`. --- ## UserInfo endpoint The `/oauth2/userinfo` endpoint returns information about the authenticated user. ### GET `/oauth2/userinfo` The user pool client makes requests to this endpoint directly and not through a browser. For more information, see [UserInfo Endpoint](http://openid.net/specs/openid-connect-core-1_0.html#UserInfo) in the OpenID Connect (OIDC) specification. ### Request parameters in header ##### `Authorization` (Required) Pass the access token using the authorization header field. ``` Authorization: Bearer ``` ### Examples positive responses ##### Sample request ``` GET https://api.medplum.com/oauth2/userinfo Authorization: Bearer ``` ##### Sample response ``` HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 { "sub": "248289761001", "name": "Jane Doe", "given_name": "Jane", "family_name": "Doe", "preferred_username": "j.doe", "email": "janedoe@example.com" } ``` For a list of OIDC claims, see [Standard Claims](http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims). :::note[Contact Information] The `email` and `phone` values of the `/userinfo` endpoint are generated from the user's [Profile resource](/docs/fhir-datastore/profiles). ::: ### Sample negative responses ##### Invalid request ``` HTTP/1.1 400 Bad Request WWW-Authenticate: error="invalid_request", error_description="Bad OAuth2 request at UserInfo Endpoint" ``` ###### `invalid_request` The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed. ##### Invalid token ``` HTTP/1.1 401 Unauthorized WWW-Authenticate: error="invalid_token", error_description="Access token is expired, disabled, or deleted, or the user has globally signed out." ``` ###### `invalid_token` The access token is expired, revoked, malformed, or invalid. --- ## Bot Endpoint import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ### POST `/admin/projects/:projectId/bot` Creates a new [Medplum Bot](/docs/). Posting to this endpoint creates a [`Bot`](/docs/api/fhir/medplum/bot) resource and a corresponding [`ProjectMembership`](/docs/api/fhir/medplum/projectmembership) resource. #### Parameters ```ts { name: string; description?: string; accessPolicy?: Reference; } ``` #### Example request **Typescript** ```ts await medplum.post('admin/projects/:projectId/bot', { name: 'Hello World Bot', description: 'Hello world', accessPolicy: { reference: 'AccessPolicy/access-policy-id', }, }); ``` **CLI** ```bash medplum post admin/projects/:projectId/bot \ '{ "name": "Hello World Bot", "description": "Hello world", "accessPolicy": { "reference": "AccessPolicy/access-policy-id" } }' ``` **cURL** ```bash curl https://api.medplum.com/admin/projects/:projectId/bot \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Hello World Bot", "description": "Hello world", "accessPolicy": { "reference": "AccessPolicy/:access-policy-id" }' ``` #### Example Response ```ts { resourceType: 'Bot', name: 'Hello World Bot', description: 'Hello world', runtimeVersion: 'awslambda', sourceCode: { contentType: 'text/typescript', title: 'index.ts', url: 'Binary/:uuid' }, id: ':bot-uuid', meta: { project: ':projectId', //... }, } ``` --- ## Client Application Endpoint import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ### POST `/admin/projects/:projectId/client` Creates a new [ClientApplication](/docs/api/fhir/medplum/clientapplication). Posting to this endpoint creates a [`ClientApplication`](/docs/api/fhir/medplum/clientapplication) resource and a corresponding [`ProjectMembership`](/docs/api/fhir/medplum/projectmembership) resource. #### Parameters ```ts { name: string; description?: string; redirectUri?: string; accessPolicy?: Reference; identityProvider?: { authorizeUrl?: string; tokenUrl?: string; userInfoUrl?: string; clientId?: string; clientSecret?: string; useSubject?: boolean; } } ``` #### Example request **Typescript** ```ts await medplum.post('admin/projects/:projectId/client', { name: 'Hello World Client', description: 'Client App for Medplum Hello World', redirectUri: 'https://example.com/redirect', accessPolicy: { reference: 'AccessPolicy/access-policy-id', }, }); ``` **CLI** ```bash medplum post admin/projects/:projectId/client \ '{ "name": "Hello World Client", "description": "Client App for Medplum Hello World", "redirectUri": "https://example.com/redirect" "accessPolicy": { "reference": "AccessPolicy/access-policy-id" } }' ``` **cURL** ```bash curl https://api.medplum.com/admin/projects/:projectId/client \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Hello World Client", "description": "Hello world", "redirectUri": "https://example.com/redirect" "accessPolicy": { "reference": "AccessPolicy/:access-policy-id" }' ``` #### Example Response ```ts { meta: { project: ':projectId', //... }, resourceType: 'ClientApplication', name: 'Hello World Client', id: ':clientId', secret: ':clientSecret', description: 'Client App for Medplum Hello World', redirectUri: 'https://example.com/redirect' } ``` --- ## Invite User Endpoint import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ### POST `/admin/projects/:projectId/invite` Invite a new user to the project. This will perform the following actions: 1. Search for an existing user with the provided `email`, if given 2. Search for an existing profile resource ([`Patient`](/docs/api/fhir/resources/patient), [`Practitioner`](/docs/api/fhir/resources/practitioner), or [`RelatedPerson`](/docs/api/fhir/resources/relatedperson)) 3. Create a new [`User`](/docs/api/fhir/medplum/user), if no existing [`User`](/docs/api/fhir/medplum/user) was found, 1. Set the password if `password` is given 2. Generate a password reset url 4. Create a new profile resource, if no existing profile was found 5. Create a corresponding [`ProjectMembership`](/docs/api/fhir/medplum/projectmembership) resource, for the (user, profile) pair 6. Send an invite email, if `sendEmail` is `true` #### Parameters ```ts { resourceType: 'Patient' | 'Practitioner' | 'RelatedPerson'; firstName: string; lastName: string; email?: string; externalId?: string; scope?: 'project' | 'server'; password?: string; sendEmail?: boolean; membership?: Partial; upsert?: boolean; forceNewMembership?: boolean; mfaRequired?: boolean; } ``` | parameter | description | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `resourceType` | The [User's](/docs/api/fhir/medplum/user) [profile resourceType](/docs/user-management#profiles) | | `firstName`, `lastName` | The first and last names that will be assigned to user's [profile resource](/docs/user-management#profiles). Ignored if a profile resource already exists | | `email` | The email address assigned to the [User](/docs/api/fhir/medplum/user). Used to identify users within each project | | `externalId` | The unique id provided by external identity provider (if applicable). See [Using External Ids](/docs/auth/external-identity-providers) | | `password` | The [User's](/docs/api/fhir/medplum/user) password | | `scope` | The scope of the user. If `project`, the user will be scoped to the project. If `server`, the user will be a server scoped user. Defaults to `server` for Practitioners and `project` for Patients. See [server vs project scoped user guide](/docs/user-management/project-vs-server-scoped-users) | | `sendEmail` | If `true`, send an invite email to the user. If self-hosting, see our [guide on setting up SES](/docs/self-hosting/install-on-aws#setup-ses) | | `membership` | Used to override any fields of the resulting [`ProjectMembership`](/docs/api/fhir/medplum/projectmembership) resource. Common use cases include: Setting [Access Policies](/docs/access/access-policies) upon invite Overriding the default `ProjectMembership.profile` | | `upsert` | If `true`, allows updating existing users and profiles instead of creating new ones. When enabled, the invite will search for existing users and profiles and update them if found, rather than throwing an error message. | | `forceNewMembership` | If `true`, forces creation of a new [`ProjectMembership`](/docs/api/fhir/medplum/projectmembership) resource even if one already exists for the user/profile combination in the project. | | `mfaRequired` | If `true`, requires the user to set up Multi-Factor Authentication (MFA) during their first login. A MFA secret will be automatically generated for the user. See [MFA documentation](/docs/auth/mfa) for more details. | #### Constraints - Either `email` or `externalId` is required. #### Examples ##### Inviting a Practitioner **Typescript** ```ts await medplum.post('admin/projects/:projectId/invite', { resourceType: 'Practitioner', firstName: 'George', lastName: 'Washington', email: 'dr.gw@example.gov', password: 'lib3rty0rDe4th!', }); ``` **CLI** ```bash medplum post admin/projects/:projectId/invite \ '{ "resourceType": "Practitioner", "firstName": "George", "lastName": "Washington", "email": "dr.gw@example.gov", "membership": { "admin": true } }' ``` **cURL** ```bash curl https://api.medplum.com/admin/projects/:projectId/invite \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "resourceType": "Practitioner", "firstName": "George", "lastName": "Washington", "email": "dr.gw@example.gov", "membership": { "admin": true } }' ``` Example Response: Returns the [`ProjectMembership`](/docs/api/fhir/medplum/projectmembership) associated with the new user ```ts { resourceType: 'ProjectMembership', id: ':id', admin: true, project: { reference: 'Project/:projectId', }, user: { reference: 'User/:userId', display: 'dr.gw@example.gov' }, profile: { reference: 'Practitioner/:practitionerId', display: 'George Washington' }, } ``` ##### Inviting a Patient **Typescript** ```ts await medplum.post('admin/projects/:projectId/invite', { resourceType: 'Patient', firstName: 'George', lastName: 'Washington', email: 'patient.gw@example.gov', password: 'lib3rty0rDe4th!', }); ``` **CLI** ```bash medplum post admin/projects/:projectId/invite \ '{ "resourceType": "Patient", "firstName": "George", "lastName": "Washington", "email": "patient.gw@example.gov", "password: "lib3rty0rDe4th!" }' ``` **cURL** ```bash curl https://api.medplum.com/admin/projects/:projectId/invite \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "resourceType": "Patient", "firstName": "George", "lastName": "Washington", "email": "patient.gw@example.gov", "password: "lib3rty0rDe4th!" }' ``` Example Response: Returns the [`ProjectMembership`](/docs/api/fhir/medplum/projectmembership) associated with the new user ```ts { resourceType: 'ProjectMembership', id: ':id', admin: true, project: { reference: 'Project/:projectId' }, user: { reference: 'User/:userId', display: 'patient.gw@example.gov' }, profile: { reference: 'Patient/:patientId', display: 'George Washington' } } ``` ##### Inviting a User with MFA Required **Typescript** ```ts await medplum.post('admin/projects/:projectId/invite', { resourceType: 'Practitioner', firstName: 'Jane', lastName: 'Doe', email: 'jane.doe@example.com', mfaRequired: true, }); ``` **CLI** ```bash medplum post admin/projects/:projectId/invite \ '{ "resourceType": "Practitioner", "firstName": "Jane", "lastName": "Doe", "email": "jane.doe@example.com", "mfaRequired": true }' ``` **cURL** ```bash curl https://api.medplum.com/admin/projects/:projectId/invite \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "resourceType": "Practitioner", "firstName": "Jane", "lastName": "Doe", "email": "jane.doe@example.com", "mfaRequired": true }' ``` When `mfaRequired: true` is set, the user will be required to enroll in Multi-Factor Authentication during their first login. See [MFA documentation](/docs/auth/mfa) for more details. ### See Also - [User Admin Guide](/docs/user-management) - [Invite a new user](/docs/app/invite) - [Custom Emails](/docs/user-management/custom-emails) - [Multi-Factor Authentication (MFA)](/docs/auth/mfa) - For details on MFA enrollment and usage --- ## React Components Medplum ships a set of React components to help you speed up your app development. We've found these components to be especially useful in the healthcare domain, and we use them ourselves in out [Medplum Web App](../app). We're adding new components all the time, and [we welcome PRs!](../contributing) You can preview our components below, or checkout our [Storybook](https://storybook.medplum.com/). --- ## Overview (Scim) Medplum user management supports [SCIM](https://www.simplecloud.info/) (System for Cross-domain Identity Management), the open API for managing identities. The base URL for hosted Medplum SCIM is https://api.medplum.com/scim/v2/ --- ## Users Medplum supports the standard SCIM `Users` API endpoints. ### Schemas SCIM resources include a `schemas` property that declares which schemas a resource conforms to. The base `User` schema is `urn:ietf:params:scim:schemas:core:2.0:User`. ### User Type and Username Medplum users always have a corresponding FHIR "profile" resource, which can be a `Patient`, `Practitioner`, or `RelatedPerson`. To specify the desired FHIR resource type, you must include a `userType` property with the FHIR resource type. For example, consider this `Practitioner`: ```json { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userType": "Practitioner", "name": { "givenName": "Alice", "familyName": "Smith" }, "emails": [{ "value": "alice@example.com" }] } ``` For example, consider this `Patient`: ```json { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userType": "Patient", "name": { "givenName": "Bob", "familyName": "Jones" }, "emails": [{ "value": "bob@example.com" }] } ``` The SCIM `userName` property will be the system generated FHIR resource ID. This can be used in combination with the SCIM `userType` to identify the FHIR resource for the user. ### Create a user Create a user by making an HTTP `POST` request to `https://api.medplum.com/scim/v2/Users` ```bash curl https://api.medplum.com/scim/v2/Users \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userType": "Practitioner", "name": { "givenName": "Alice", "familyName": "Smith" }, "userName": "alice@example.com", "emails": [ { "value": "alice@example.com" } ] }' ``` ### Read a user Read a user by making an HTTP `GET` request to `https://api.medplum.com/scim/v2/Users/{id}` ```bash curl https://api.medplum.com/scim/v2/Users/MY_USER_ID \ -H "Authorization: Bearer MY_ACCESS_TOKEN" ``` ### Update a user Update a user by making an HTTP `PUT` request to `https://api.medplum.com/scim/v2/Users/{id}` ```bash curl -X PUT https://api.medplum.com/scim/v2/Users \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userType": "Practitioner", "id": "41ecbf96-8296-4fac-801c-5e78042ba436", "name": { "givenName": "Alice", "familyName": "Smith" }, "userName": "alice@example.com", "emails": [ { "value": "alice@example.com" } ], "externalId": "test-external-id" }' ``` ### Delete a user Delete a user by making an HTTP `DELETE` request to `https://api.medplum.com/scim/v2/Users/{id}` ```bash curl -X DELETE https://api.medplum.com/scim/v2/Users/MY_USER_ID \ -H "Authorization: Bearer MY_ACCESS_TOKEN" ``` Note that deleting the user does not delete any of the corresponding FHIR resources. ### Search users Search for users by making an HTTP `GET` request to `https://api.medplum.com/scim/v2/Users` ```bash curl https://api.medplum.com/scim/v2/Users \ -H "Authorization: Bearer MY_ACCESS_TOKEN" ``` --- ## The Admin Page The [Admin Page](https://app.medplum.com/admin/project) of the Medplum App allows admin users to view and edit details of their project that normal users do not have access to. At the top of the page, there is an array of tabs as shown below. In this guide, we will briefly go over the content of each tab as well as any functionality they provide. ![App Admin Page](./admin-page.png) ### Details The Details tab displays all of the populated elements of the current [`Project`](/docs/api/fhir/medplum/project) resource. For more details on the [`Project`](/docs/api/fhir/medplum/project) resource, see the [User Management Guide](/docs/user-management). ### Users The Users tab displays all of the [`Practitioner`](/docs/api/fhir/resources/practitioner) resources that are also a [`User`](/docs/api/fhir/medplum/user) in your project. It also allows you to invite new users. For more details on the [`User`](/docs/api/fhir/medplum/user) resource, see the [User Management Guide](/docs/user-management). ### Patients The Patients tab displays all of the [`Patient`](/docs/api/fhir/resources/patient) resources that are also a [`User`](/docs/api/fhir/medplum/user) in your project. It also allows you to invite new patients. For more details on the [`User`](/docs/api/fhir/medplum/user) and [`Patient`](/docs/api/fhir/resources/patient) resources see the [User Management Guide](/docs/user-management). ### Clients The Clients tab displays all of the [`ClientApplication`](/docs/api/fhir/medplum/clientapplication) resources that are associated with your project. It also allows you to create new [`ClientApplication`](/docs/api/fhir/medplum/clientapplication). For more details, see the [Authentication Method docs](/docs/auth/token-exchange#set-up-your-clientapplication). ### Bots The Bots tab dispalys all of the [`Bots`](/docs/api/fhir/medplum/bot) that are a part of your project and allows you to create new ones. For more details on [`Bots`](/docs/api/fhir/medplum/bot) see the [Bot Basics docs](/docs/bots/bot-basics). ### Secrets The Secrets tab displays all of your project secrets as well as allowing you to create new ones. Secrets are used to store sensitive information and as access controls. For example, API keys, [`Bot`](/docs/api/fhir/medplum/bot) secrets, and reCAPTCHA secrets would all appear here. For more details see the [Project Secrets docs](/docs/access/projects#project-secrets). ### Sites The Sites tab allows you to view and edit any custom domains that your project is configured for. --- ## Introduction The Medplum App is a user-facing web application available at [https://app.medplum.com/](https://app.medplum.com/) and is targeted towards developers and project administrators. This administrative app is developed from the [Medplum React Components](/docs/react), meaning that many of these views and functionality can be embedded into your own custom apps. This guide will go over how to navigate the basic functionality of the Medplum App. ### Homepage ![App Homepage](./app-homepage.png) When you first log in to the Medplum App, you'll see the page shown above. This home page consists of a few primary components: - **[Search Control](#search-control)** - Displays a list of the currently selected resource type. - **[Sidebar](#sidebar)** - Used to select the current resource type and quickly navigate to different parts of the Medplum App. - **[Quick Search Bar](#quick-search-bar)** - Used to search for specific resource instances. - **[Profile Selector](#profile-selector)** - Displays profile information for the currently logged in user. ### Search Control The [Search Control](https://storybook.medplum.com/?path=/story/medplum-searchcontrol--checkboxes) displays a paginated list of all the resources for the selected resource type. You can select individual resources for certain operations using the checkbox on the left hand, and you can select all resources on the current page by clicking on the checkbox in the table header. The toolbar at the top contains actions that you can use to customize this view. - **Fields:** Can be used to add/remove columns for resource elements - **Filters:** Used to filter rows based on search criteria. Only elements with [search parameters](/docs/search/basic-search#search-parameters) can be filtered on. You can also use the filter icon at the top of each column to filter on a specific element. Refer to our [search guide](/docs/search/basic-search) for more information. - **New:** Create a new resource of the selected type. This will open a form to specify the fields of the newly created resource, with the \* character indicating required fields. :::note[] This form is an instance of the [`ResourceForm`](https://storybook.medplum.com/?path=/story/medplum-resourceform--patient) React component, and can be embedded into your own applications. ::: - **Export:** Export the resources in the current view, either as a CSV or as [FHIR Bundle](/docs/api/fhir/resources/bundle). :::note[] This operation will export _all_ resources for the selected filters, not just those on the current page (up to a maximum of 1000 resources). ::: - **Delete:** Delete the selected resources. You can perform a bulk delete by selecting multiple resources at once. - **Bulk:** Execute a [Medplum Bot](/docs/bots) on all selected rows. This search control is also encapsulated as the [`Search Control`](https://storybook.medplum.com/?path=/story/medplum-searchcontrol--checkboxes) React component and can be embedded into your own applications. :::caution[Caution] For performance reasons, the total resource count displayed on the Medplum App is an **estimated count**, not an exact count. You can read more about fetching resource counts [here](/docs/search/paginated-search#getting-the-total-number-of-results-with-_total) ::: ### Sidebar The sidebar is used to quickly navigate to different parts of the Medplum App. You can show/hide the sidebar by clicking the **plum icon** in the top-left corner. By default, the sidebar is populated with shortcuts to common resources, but you can use the **Resource Type Input** to navigate search for any FHIR resource type. The sidebar also contains links to administrative pages, including: - [Project Admin page](https://app.medplum.com/admin/project) - [Access Policies](/docs/access/access-policies) - [Subscriptions](/docs/subscriptions) - [Batch Upload Tool](/docs/tutorials/importing-sample-data#batch-upload-tool) - [Security Settings](https://app.medplum.com/security) ### Quick Search Bar The Quick Search Bar can be used to navigate to specific instances of [ServiceRequest](/docs/api/fhir/resources/servicerequest) or [Patient](/docs/api/fhir/resources/patient) resources, which are two of the most common resource types. This search bar searches the following search parameters: - `Patient.id` - `Patient.name` - `Patient.identifier` - `ServiceRequest.id` - `ServiceRequest.identifier` ### Profile Selector The profile selector can be accessed by clicking the arrow next to your name on the top-right corner of the screen. By clicking "Switch to another project", users can be log into multiple projects simultaneously. The profile selector will display all projects that the user is currently logged into so that users can quickly switch between projects. "Account Settings" can be used to navigate to the user's [profile resource](/docs/app/invite) page. Lastly, the profile selector contains the application's current version number (e.g. `2.0.25-427e07af`), which can be useful information when troubleshooting. --- ## The `Apps` Tab ## The `Apps` Tab The Medplum App allows for easy integration of workflows and third-party applications using the `Apps` tab. It is important to reiterate that the Medplum App is targeted towards developers and the `Apps` tab is primarily a tool to help with developer automation. The `Apps` tab is available on the resource page of all resource types. At the moment, it serves `Questionnaire` resources and [SMART App Launch](/docs/integration/smart-app-launch), but additional functionality is being worked on to add to it. ### Navigating to the `Apps` Tab To get to the `Apps` tab of, for example, a patient go to the `Patient` resources page by clicking Patient on the left sidebar of the app or navigating to https://app.medplum.com/Patient. Select the patient you would like to view to go to their resource page. From here, select the `Apps` tab from the array of tabs at the top of the page. ![Apps Tab](apps-tab.png) Alternatively, you can navigate directly to this page at https://app.medplum.com/Patient/:id/apps. ### Questionnaires The `Apps` tab of a resource will display all `Questionnaire` resources that are assigned to that `ResourceType` by the `Questionnaire.subjectType` field. For example, a `Patient` page will display all `Questionnaire` resources that are applicable to patients. This can streamline workflows and make it easier for users to find any forms or surveys they may need to complete. ### SMART App Launch SMART App Launch allows you to easily launch third-party apps from within the Medplum App, while maintaining access to authorized data. This is currently available on the `Apps` tab of all `Patient` and `Encounter` resource pages on the Medplum App. When you launch an app from a `Patient` page using SMART, the app will have access to that patient's data. The specific data that the new app has access to can be configured so that only certain data is authorized to be shared. This allows for an easy and user-friendly integration of third-party apps with the Medplum App. If you have set up an app with SMART, but do not see it in the `Apps` tab, it may not be configured correctly. For details on setup and configuration, see the [SMART App Launch Integration docs](/docs/integration/smart-app-launch). --- ## Medplum App The Medplum App is a user-facing web application available at [https://app.medplum.com/](https://app.medplum.com/) targeted towards developers and project administrators. The app is a quick way to administer projects, manage users, and inspect resources. You can use the Medplum app to configure or view resources required for application development like credentials, automations and more. ### Getting Started 1. [Register](https://app.medplum.com/register) a new account and project 2. [Learn](./app/app-introduction) about the App interface 3. [Invite](./app/invite) another user to your project 4. [Import a patient](/docs/tutorials/importing-sample-data) ### Reference The Medplum App is [open source](https://github.com/medplum/medplum/tree/main/packages/app). Browse a detailed list of [features and fixes](https://github.com/medplum/medplum/pulls?q=is%3Apr+label%3Amedplum-app) for more context. It is built off of the [React](/docs/react) component library. --- ## Invite a user This guide explains how to invite another user to your Medplum project. import adminUsersPage from './admin-users-page.png' 1. Open the Medplum app in your browser: https://app.medplum.com/. If you don't have an account yet, see the [Register](../tutorials/register) page. 2. Navigate to the [Project Admin](https://app.medplum.com/admin/project) page at [https://app.medplum.com/admin/project](https://app.medplum.com/admin/project). 3. Click on the "Users" tab. 4. Click on the "Invite new user" link at the bottom of the screen. import inviteNewUser from './invite-new-user.png' 5. Select the new user's Role. A user's role defines which FHIR resource type represents the user in this project, and can take one of the following values: 1. **Practitioner:** This is the most common profile type. Applies to any user who is involved in administering care, including physicians, technicians, engineers, IT staff, and customer service representatives. 2. **Patient:** This is the profile for any users who are the beneficiary of care. 3. **RelatedPerson:** This profile represents users who are related to a patient and need access to some of their clinical data, but who don't benefit directly from care. This is typically used for parents/guardians for pediatric patients, or spouses of patients who aren't being treated themselves. 6. Fill in the user details such as first name, last name, and email address. 7. For advanced security features, you can configure an [AccessPolicy](/docs/access/access-policies). An AccessPolicy allows you to specify which resource types users can read and write. For **Patient** users, if no access policy is selected here, the project's `defaultPatientAccessPolicy` will be applied automatically. See [Project Settings](/docs/access/projects#settings) for how to configure that project-level default. 8. By default, Medplum will send a welcome email to the new user. You can toggle the "Send email" checkbox if you do not want to send an email. (You may also be interested in [Custom Emails](/docs/user-management/custom-emails)). 9. You can optionally add the new user as a Project Administrator by checking the "Admin" checkbox. See the [User Admin Guide](/docs/user-management#user-administration-via-medplum-app) for more details and how to do this using the API. 10. By default, Practitioners are created as **server-scoped users**, meaning their `User` resource exists at the server level and can be added to multiple Medplum projects (useful for developers and administrators). Checking **"Project Scoped"** creates a **project-scoped user** whose `User` resource is tied exclusively to this project. Project-scoped users are recommended for real practicing clinicians and patients who will only interact with a single production project. Note that project-scoped Practitioners must be invited separately into each project they need access to. For more details and implications (such as custom email flows and password resets), see [Project vs Server Scoped Users](/docs/user-management/project-vs-server-scoped-users). 11. To require the new user to enroll in Multi-Factor Authentication (MFA), check the **"MFA Required"** checkbox. When enabled, an MFA secret is automatically generated for the user. On their first login they will be prompted to configure an authenticator app (e.g. Google Authenticator, Microsoft Authenticator, or Authy) before gaining access. See [Multi-Factor Authentication](/docs/auth/mfa) for more details. 12. Click "Invite" when the user details are ready. When you click "Invite", the system will send an email to the user with instructions how to activate their new account. --- ## The Sign In Page Once a [`User`](/docs/api/fhir/medplum/user) accepts your invite, they will be able to log in to the [Medplum App](https://app.medplum.com) at https://app.medplum.com/signin. ![Sign In Page](./sign-in-screenshot.png) If the [`User`](/docs/api/fhir/medplum/user) is a member of multiple [`Projects`](/docs/api/fhir/medplum/project), logging in will bring them to the Project Selector page. Choose a project to progress to it's homepage. To switch between projects after you have logged in, you can use the [Profile Selector](/docs/app/app-introduction/index.md#profile-selector). If the [`User`](/docs/api/fhir/medplum/user) is a member of only one [`Project`](/docs/api/fhir/medplum/project), they will progress directly from the sign in page to that project's homepage. ### Signing In as a Patient By default the sign in page only allows [`Users`](/docs/api/fhir/medplum/user) to sign in if they are a [`Practitioner`](/docs/api/fhir/resources/practitioner). To allow [`Patients`](/docs/api/fhir/resources/patient) to sign in you will need to add the `project=:id` query param to the url. This ensures that [`Patients`](/docs/api/fhir/resources/patient) can only log in to a single project at a time. The full url will be as follows: https://app.medplum.com/signin?project=:id, where `:id` is the `id` of the [`Project`](/docs/api/fhir/medplum/project) they are logging into. --- ## Client Assertion (JWT Bearer) import BrowserOnlyTabs from '@site/src/components/BrowserOnlyTabs'; import TabItem from '@theme/TabItem'; Client Assertion is an alternative to the [Client Credentials](./client-credentials) flow that authenticates the client using a **signed JWT** instead of a shared client secret. The client signs the JWT with a private key; Medplum verifies it against the public keys published at the client's JWKS URI. This approach is specified in [RFC 7523 — JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://datatracker.ietf.org/doc/html/rfc7523) and is a cornerstone of the [SMART on FHIR Backend Services](https://hl7.org/fhir/smart-app-launch/backend-services.html) specification. ### When to Use Client Assertion Prefer client assertion over a client secret when: - A specification **requires** it — SMART Backend Services and some payer integrations mandate JWT-based client authentication. - You want to **avoid transmitting shared secrets** and instead rely on asymmetric cryptography. - You need **per-instance key rotation** without updating a central secret. ### Prerequisites 1. A Medplum account. (If not, please [register](https://app.medplum.com/register).) 2. A [ClientApplication](https://app.medplum.com/ClientApplication) — create one on the [Project Admin page](https://app.medplum.com/admin/clients). 3. An asymmetric key pair (RSA or EC). See [Generating a Key Pair](#generating-a-key-pair) below. 4. A publicly accessible JWKS endpoint that hosts your public key. ### Generating a Key Pair You can generate a key pair with any standard tool. The examples below use `openssl`. **EC (P-384, recommended):** ```bash # Generate private key openssl ecparam -name secp384r1 -genkey -noout -out private.pem # Derive public key openssl ec -in private.pem -pubout -out public.pem ``` **RSA (2048-bit minimum):** ```bash # Generate private key openssl genrsa -out private.pem 2048 # Derive public key openssl rsa -in private.pem -pubout -out public.pem ``` Keep the **private key** secret. Publish only the **public key** via a JWKS endpoint (see below). Medplum supports the following signing algorithms: `ES256`, `ES384`, `ES512`, `RS256`, `RS384`, `RS512`. ### Publishing Your Public Key (JWKS) Medplum fetches your public key at runtime from the URL you configure on the `ClientApplication`. The endpoint must: - Be publicly accessible over HTTPS. - Return a JSON object with a `keys` array conforming to [RFC 7517 — JSON Web Key](https://datatracker.ietf.org/doc/html/rfc7517). **Example JWKS response:** ```json { "keys": [ { "kty": "EC", "crv": "P-384", "use": "sig", "kid": "my-key-1", "x": "", "y": "" } ] } ``` Many platforms (AWS, GCP, Auth0, etc.) can host a JWKS endpoint for you. If you are self-hosting, you can use `openssl` or a library like [`jose`](https://github.com/panva/jose) to convert your PEM public key to JWK format. ### Configuring the ClientApplication Set the `jwksUri` field on your `ClientApplication` to the URL of your JWKS endpoint. You can do this in the Medplum App UI or via the FHIR API: ```json { "resourceType": "ClientApplication", "name": "My Backend Service", "jwksUri": "https://example.com/.well-known/jwks.json" } ``` ### Obtaining a Token #### JWT Claims The JWT you sign must include the following claims: | Claim | Required | Value | |-------|----------|-------| | `iss` | Yes | Your `ClientApplication` ID (the UUID) | | `sub` | Yes | Your `ClientApplication` ID (the UUID) | | `aud` | Yes | The Medplum token endpoint URL (e.g. `https://api.medplum.com/oauth2/token`) | | `iat` | Yes | Issued-at time (Unix epoch seconds) | | `exp` | Yes | Expiration time (Unix epoch seconds); keep this short (≤ 5 minutes) | | `jti` | Recommended | A unique identifier for the JWT to prevent replay attacks | #### Token Request Once you have a signed JWT, exchange it for an access token with a `client_credentials` grant: **cURL** ```bash JWT="" curl -X POST https://api.medplum.com/oauth2/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=$JWT" ``` **TypeScript** ```typescript import { MedplumClient } from '@medplum/core'; import { SignJWT, importPKCS8 } from 'jose'; import { readFileSync } from 'fs'; const CLIENT_ID = 'your-client-application-id'; const TOKEN_URL = 'https://api.medplum.com/oauth2/token'; // Must match your private key type: ES384 for a P-384 EC key, RS384 for an RSA key, etc. const ALG = 'ES384'; async function main(): Promise { // Load your private key (keep this secret!) const privateKeyPem = readFileSync('private.pem', 'utf8'); const privateKey = await importPKCS8(privateKeyPem, ALG); // Build and sign the JWT const jwt = await new SignJWT({}) .setProtectedHeader({ alg: ALG }) .setIssuedAt() .setIssuer(CLIENT_ID) .setSubject(CLIENT_ID) .setAudience(TOKEN_URL) .setExpirationTime('5m') .sign(privateKey); // Authenticate with Medplum const medplum = new MedplumClient(); await medplum.startJwtAssertionLogin(jwt); // Now make authenticated requests const patients = await medplum.searchResources('Patient'); console.log(patients); } main().catch(console.error); ``` **Python** ```python import time import uuid import requests from cryptography.hazmat.primitives import serialization from cryptography.hazmat.backends import default_backend import jwt # pip install PyJWT cryptography CLIENT_ID = 'your-client-application-id' TOKEN_URL = 'https://api.medplum.com/oauth2/token' # Load your private key with open('private.pem', 'rb') as f: private_key = serialization.load_pem_private_key(f.read(), password=None, backend=default_backend()) now = int(time.time()) payload = { 'iss': CLIENT_ID, 'sub': CLIENT_ID, 'aud': TOKEN_URL, 'iat': now, 'exp': now + 300, # 5 minutes 'jti': str(uuid.uuid4()), } # The signing algorithm must match the key type: # - ES384 requires an EC private key (e.g. P-384) # - RSA private keys should use RS256 or RS384 instead signed_jwt = jwt.encode(payload, private_key, algorithm='ES384') response = requests.post(TOKEN_URL, data={ 'grant_type': 'client_credentials', 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 'client_assertion': signed_jwt, }, headers={'Content-Type': 'application/x-www-form-urlencoded'}) access_token = response.json()['access_token'] ``` On success, the response is a standard OAuth2 token response: ```json { "token_type": "Bearer", "access_token": "", "expires_in": 3600 } ``` The `access_token` can then be used in subsequent API requests as a `Bearer` token. ### Error Reference | `error_description` | Cause | |---------------------|-------| | `Invalid client assertion` | The `client_assertion` field is empty or not a valid JWT. | | `Client not found` | The `iss`/`sub` in the JWT does not match any `ClientApplication` ID. | | `Client must have a JWK Set URL` | The matching `ClientApplication` has no `jwksUri` configured. | | `Invalid client assertion audience` | The `aud` claim does not match the Medplum token endpoint URL. | | `Invalid client assertion issuer` | The `iss` claim does not match the `ClientApplication` ID. | | `Invalid client assertion signature` | The JWT signature could not be verified against any key in the JWKS. | | `Unsupported client assertion type` | The `client_assertion_type` value is not `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`. | ### Further Reading - [RFC 7523 — JWT Profile for OAuth 2.0 Client Authentication](https://datatracker.ietf.org/doc/html/rfc7523) - [RFC 7517 — JSON Web Key (JWK)](https://datatracker.ietf.org/doc/html/rfc7517) - [SMART on FHIR Backend Services](https://hl7.org/fhir/smart-app-launch/backend-services.html) - [Client Credentials Flow](./client-credentials) - [Mutual TLS (mTLS)](./mtls) --- ## Client Credentials import BrowserOnlyTabs from '@site/src/components/BrowserOnlyTabs'; import TabItem from '@theme/TabItem'; The Medplum API uses standard OAuth2/OpenID authentication. The "Client Credentials Flow" is recommended for machine-to-machine access. ### Obtaining Credentials This tutorial assumes you already have a Medplum account. (If not, please [register](https://app.medplum.com/register).) Create a [ClientApplication](https://app.medplum.com/ClientApplication) if one does not already exist. You can create a new ClientApplication on the [Project Admin page](https://app.medplum.com/admin/clients). For this example you will need the `ID` and `Secret`. ### Connecting to the service Execute an HTTP POST request to the OAuth2 Token endpoint: **cURL** ```curl curl -X POST https://api.medplum.com/oauth2/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials&client_id=$MY_CLIENT_ID&client_secret=$MY_CLIENT_SECRET" ``` **Python** ```py def get_auth_token(url, client_id, client_secret): # Encode the client ID and client secret headers = { "Content-Type": "application/x-www-form-urlencoded" } body = { "grant_type": "client_credentials", "client_id": client_id, "client_secret": client_secret } response = requests.post(url, data=body, headers=headers) return response.json()['access_token'] ``` **Note:** If you are hosting this on localhost, without editing the configuration file, the URL will be `http://localhost:8103/oauth2/token` On success, the response will be a JSON object with the following properties: ```json { "token_type": "Bearer", "access_token": "", "expires_in": 3600 } ``` The value of "access_token" can then be used in future requests for authentication. Alternatively, you can initiate the process in the Medplum Client by using the [`startClientLogin`](/docs/sdk/core.medplumclient.startclientlogin) convenience method. For more details about OAuth2 Client Credentials Flow: - https://datatracker.ietf.org/doc/html/rfc6749#section-4.4 - https://oauth.net/2/grant-types/client-credentials/ - https://auth0.com/docs/flows/client-credentials-flow --- ## Direct External Authentication Medplum supports authenticating users directly with an external Identity Provider (IDP) access token, without requiring a token exchange or authorization code flow. This is useful when your application already holds a valid JWT from an external IDP and wants to access the Medplum API directly. :::caution[Self-Hosted Deployments Only] This feature requires super admin privileges to configure `externalAuthProviders` in the [server config](/docs/self-hosting/server-config), which is only available on self-hosted Medplum deployments. If you are using Medplum's cloud-hosted service and need this capability, please contact [Medplum support](/contact). ::: ### Overview When a user presents a JWT issued by a configured external auth provider, Medplum can: 1. Validate the token against the IDP's userinfo endpoint 2. Identify the user from claims in the token 3. Return Medplum auth credentials for subsequent API access There are two ways to identify the user from the token: | Method | JWT Claim | Lookup | Best for | | :--- | :--- | :--- | :--- | | **FHIR User** | `fhirUser` | Profile reference or search | SMART-on-FHIR compliant IDPs | | **Subject** | `sub` | `ProjectMembership.externalId` | Standard OIDC IDPs | If both `fhirUser` and `sub` are present, the `fhirUser` claim takes precedence. ### Server Configuration Add external auth providers to your Medplum server configuration: ```json { "externalAuthProviders": [ { "issuer": "https://your-idp.example.com", "userInfoUrl": "https://your-idp.example.com/oauth2/userinfo" } ] } ``` | Field | Description | | :--- | :--- | | `issuer` | The `iss` claim value in JWTs from this IDP | | `userInfoUrl` | The IDP's userinfo endpoint, used to validate tokens | ### Using the `fhirUser` Claim The `fhirUser` claim contains a FHIR resource reference identifying the user's profile. It can be provided in several formats: - **Direct reference**: `Practitioner/abc123` - **Search string**: `Practitioner?identifier=1234567890` - **Absolute URL**: `https://idp.example.com/fhir/Practitioner?identifier=1234567890` Some IDPs place custom claims inside an `ext` block. Medplum also checks `ext.fhirUser` for compatibility. #### How it works 1. Medplum extracts the `fhirUser` claim from the JWT 2. Validates the token against the IDP's userinfo endpoint 3. Searches for the FHIR profile resource matching the claim 4. Finds the `ProjectMembership` associated with that profile 5. Returns auth credentials scoped to that membership ### Using the `sub` Claim The `sub` (subject) claim is a standard JWT claim (RFC 7519) present in virtually all JWTs. When no `fhirUser` claim is present, Medplum uses the `sub` claim to look up a `ProjectMembership` by its `externalId` field. #### Setup 1. Configure the external auth provider in your server config (see above) 2. Invite users with an `externalId` matching their IDP `sub` value: ```bash curl 'https://BASE_URL/admin/projects/PROJECT_ID/invite' \ -H 'Authorization: Bearer ACCESS_TOKEN' \ -H 'Content-Type: application/json' \ --data-raw '{ "resourceType": "Practitioner", "firstName": "Jane", "lastName": "Smith", "externalId": "IDP_SUBJECT_ID" }' ``` Replace `BASE_URL`, `PROJECT_ID`, and `ACCESS_TOKEN` with your actual values. The `externalId` value should match the `sub` claim that the IDP assigns to this user. :::tip[] To find a user's `sub` value, decode the JWT from your IDP or check the IDP's user management console. For Auth0, the subject may be prefixed with the identity provider (e.g., `google-oauth2|110925489055200000000`). Use the full value as the `externalId`. ::: #### How it works 1. Medplum parses the JWT and finds no `fhirUser` claim 2. Extracts the `sub` claim 3. Validates the token against the IDP's userinfo endpoint 4. Searches for a `ProjectMembership` where `externalId` matches the `sub` value 5. Returns auth credentials scoped to that membership :::caution[Uniqueness] The `externalId` must be unique across all project memberships. If multiple memberships share the same `externalId`, authentication will fail with a `401` response to prevent ambiguity. ::: ### Making Requests Once configured, make requests to the Medplum FHIR API using the external IDP token directly: ```bash curl 'https://${baseUrl}/fhir/R4/Patient' \ -H 'Authorization: Bearer ${externalAccessToken}' ``` A successful response means that the authentication with the external IDP was successful. Medplum caches external auth results in Redis for 1 hour to minimize calls to the IDP's userinfo endpoint. ### Comparison with Token Exchange | | Direct External Auth | [Token Exchange](/docs/auth/token-exchange) | | :--- | :--- | :--- | | **Endpoint** | `/oauth2/userinfo` | `/oauth2/token` | | **Configuration** | Server-level `externalAuthProviders` | Per-`ClientApplication` identity provider | | **Token format** | Uses external JWT directly | Exchanges for Medplum access token | | **User lookup** | `fhirUser` claim or `sub` / `externalId` | Email or `sub` (via `useSubject` flag) | Choose **Direct External Auth** when you want to use external JWTs directly without an explicit token exchange step. Choose **Token Exchange** when you need a standard Medplum access token or when the auth flow is scoped to a specific `ClientApplication`. --- ## Domain-level Identity Providers A Domain-level Identity Provider (DL-IDP) is a server-level configuration that sets up an external identity provider for all users from a given domain. This identity provider will be used for _all_ Medplum applications the user logs into, **including the Medplum App**. Domain-level providers are primarily used to ensure that all practitioners access Medplum data via your corporate identity solution. Once a DL-IDP is enabled for mydomain.com, any user that attempts to sign in with an email on that domain (name@mydomain.com) will be prompted to authenticate using the configured provider. Enabling DL-IDPs will apply to _all_ emails at that domain, including dev accounts and name+text@mydomain.com type emails. This guide demonstrates how to set up a DL-IDP, using Okta as an example provider. :::caution[Note] Setting up DL-IDPs on the Medplum Hosted instance requires an Enterprise account. If [self-hosting](/docs/self-hosting), setting it up requires super admin privileges. ::: ### Example: Setting up Okta as a Domain-level Identity Provider #### Okta Setup Enabling Okta requires configuration on the Okta side and the Medplum side. You will need to be an Okta admin to set up a App Integration on Okta. Click on "Create App Integration" ![Okta Applications Page](/img/auth/okta-applications.png) When prompted, choose "**OIDC - OpenID Connect**" and "**Web Application**". Click "**Next**". ![Okta App Integration Page](/img/auth/okta-app-integration.png) Use the following settings: - App integration name: Choose your preferred name, or "Medplum" - Grant Type - 🔲 Client Credentials - unchecked - ☑️ Authorization Code - checked - 🔲 Refresh Token - unchecked - 🔲 Implicit (hybrid) - unchecked - Sign-in redirect URIs - 🔲 Allow wildcard - unchecked - Callback URL - "\{medplumApiServerBaseUrl\}/auth/external" - For example: - Medplum hosted apiServerBaseUrl is "https://api.medplum.com" - So the callback URL is "https://api.medplum.com/auth/external" - Sign-out redirect URIs: Leave blank - Trusted Origins: Leave blank ![Okta Integration Config Page](/img/auth/okta-integration-config.png) Scroll down to the Assignments section: - Assignments - Controlled access - Choose the level appropriate for your organization - We recommend "Allow everyone in your organization to access" as a an account on Medplum is still required to be able to access - ☑️ Enable immediate access - checked Then click "Save". ![Okta Assignments Page](/img/auth/okta-assignments.png) On the next page, note the "Client ID" and "Client Secret", take note of those - they will be needed for the Medplum set up. #### Medplum Setup To configure Okta as an external authentication provider, you will need 5 pieces of data: - Authorize URL - Token URL - UserInfo URL - Client ID - Client Secret Okta uses separate domains per organization. You will need your organization's Okta server. This is called the Okta "baseUrl". You can find it in the top-left menu of the Okta admin panel by clicking on your name when logged in. ![Okta Find URL](/img/auth/okta-find-url.png) The "baseUrl" could look something like this `https://dev-12345678.okta.com` or could be a named subdomain like `https://mydomain.okta.com`. Once you have obtained it, construct the Authorize, Token and UserInfo URLs as follows: - Authorize URL = $\{baseUrl\}/v1/authorize (e.g., "https://dev-12345678.okta.com/oauth2/v1/authorize") - Token URL = $\{baseUrl\}/oauth2/v1/token (e.g., "https://dev-12345678.okta.com/oauth2/v1/token") - UserInfo URL = $\{baseUrl\}/oauth2/v1/userinfo (e.g., "https://dev-12345678.okta.com/oauth2/v1/userinfo") Client ID and Client secret will be the same as those obtained at the end of the previous section. Create a [`DomainConfiguration`](/docs/api/fhir/medplum/domainconfiguration) resource: - Add the values for the five elements above - Leave "useSubject" **unchecked** - Set the `domain` field to the users' email domain (e.g. "mydomain.com") **in all lower-case**. Once the resource has been saved, all new authentication requests from that domain will use Okta authentication. :::caution[Note] Configuring a DL-IDP on the Medplum Hosted service requires a Medplum team member, contact us at hello@medplum.com to enable. For those self-hosting, setup below requires super admin privileges. ::: --- ## Embedded Login ### Introduction [Using Medplum as IDP](/docs/auth/medplum-as-idp) describes the OAuth2 Authorization Code flow using a full browser redirect: your user leaves your application, authenticates on a Medplum-hosted page, and is redirected back with a code. **Embedded login** is the same underlying OAuth2 Authorization Code flow with PKCE — it just skips the redirect. The email/password form is rendered directly inside your own application, using the `` React component or the [`startLogin()`](/docs/sdk/core.medplumclient.startlogin) SDK method directly. Your user never sees another domain. Choose embedded login when you want full control over your login page's branding and don't want a visible navigation away from your app. Choose the [redirect flow](/docs/auth/medplum-as-idp) instead if you'd rather the password never be typed into your own application's code at all — since Medplum's hosted page runs on its own domain, a vulnerability in your app can't expose credentials it never saw. ### How it works Both variants use the same two-request shape: authenticate to get a one-time code, then redeem that code for tokens. The difference is only in how request 1 happens — as a direct API call instead of a browser navigation. ```mermaid sequenceDiagram participant Browser as Browser / App participant Medplum as Medplum Server Note over Browser: Generate a PKCE verifier and challenge Browser->>Medplum: POST /auth/login
email, password, clientId, codeChallenge Note over Medplum: Compare password against the stored hash.
Create a Login resource with a one-time code. Medplum-->>Browser: { login, code } Browser->>Medplum: POST /oauth2/token
grant_type=authorization_code, code, client_id, code_verifier Note over Medplum: Verify code_verifier against the stored codeChallenge Medplum-->>Browser: access_token, id_token, refresh_token? ``` The password is only ever checked in the first request. The second request never sees it — it only carries the one-time `code` plus the PKCE `code_verifier` needed to redeem it. ### Prerequisites Embedded login uses the same [`ClientApplication`](/docs/auth/medplum-as-idp#create-a-client-application) setup as the redirect flow. If you haven't created one yet, follow the **Create a Client Application** steps on that page first. ### Request 1: authenticate with credentials Call `POST /auth/login` with the user's credentials: | Field | Description | | :-------------------------------------- | :---------------------------------------------------------------- | | `email` | The user's email address | | `password` | The user's password | | `clientId` | (Optional) Your `ClientApplication` ID | | `codeChallenge` / `codeChallengeMethod` | The PKCE challenge for this login attempt (see [PKCE](#pkce)) | On success, the response contains a `login` ID and a one-time `code`: ```json { "login": "1a2b3c4d-...", "code": "..." } ``` If the user has multi-factor authentication enrolled, or belongs to more than one project, the response looks different — see [Multi-factor authentication and multiple projects](#multi-factor-authentication-and-multiple-projects) below. ### Request 2: redeem the code Call [`POST /oauth2/token`](/docs/api/oauth/token) with the code from request 1: | Field | Value | | :--------------- | :---------------------------------------- | | `grant_type` | Fixed value: `authorization_code` | | `code` | The one-time code from request 1 | | `client_id` | Your `ClientApplication` ID | | `code_verifier` | The PKCE verifier that produced the `codeChallenge` sent in request 1 | A successful response contains your tokens — see [Tokens](#tokens) below. ### PKCE Both requests are bound together by [PKCE](https://www.rfc-editor.org/rfc/rfc7636) (Proof Key for Code Exchange), so that the `code` returned by request 1 can only be redeemed by whoever started the login attempt. 1. **Before request 1**, generate a random `code_verifier` and keep it locally. Hash it with SHA-256 to produce the `codeChallenge` sent to `/auth/login`. 2. Medplum stores that `codeChallenge` alongside the `Login` it creates. It never sees the raw verifier at this point. 3. **In request 2**, send the original, un-hashed `code_verifier` in the token request body. 4. Medplum hashes the verifier you just sent and checks it matches the `codeChallenge` stored in step 2. A mismatch is rejected. If you use the Medplum SDK (`startLogin()` or ``), this is handled for you automatically — the verifier is generated, stored, and replayed without any code on your part. :::note[PKCE is required by default] A `ClientApplication` can opt out of requiring PKCE by setting `pkceOptional: true`, in which case a client secret can be used instead. This isn't recommended for browser-based applications, since a secret embedded in client-side code isn't actually secret. ::: ### Multi-factor authentication and multiple projects Request 1's response isn't always `{ login, code }`. A few other cases are possible before a code is issued: | Response field | Meaning | What to do next | | :-------------------- | :------------------------------------------------------------ | :---------------------------------------------- | | `mfaRequired` | The user has MFA enrolled and must provide a code | `POST /auth/mfa/verify` | | `mfaEnrollRequired` | The project requires MFA and the user hasn't enrolled yet. This response already includes a TOTP enrollment URI and QR code. | `POST /auth/mfa/login-enroll`, submitting the code generated by the authenticator app the user just set up from that QR | | `memberships` | The user belongs to more than one project | `POST /auth/profile`, naming which membership to use | Each of these follow-up calls returns the same shape as request 1 — either another prompt, or the `{ login, code }` you need to move on to request 2. See [Multi-Factor Authentication](/docs/auth/mfa) for full details on the MFA enrollment and verification flow. ### Tokens A successful token exchange returns: | Token | Contents | | :--------------- | :---------------------------------------------------------------- | | `access_token` | A JWT used as a `Bearer` token on every subsequent API request | | `id_token` | OpenID Connect identity claims for the logged-in user (`fhirUser`, `auth_time`, and `email` if the `email` scope was requested) | | `refresh_token` | Present only if requested — see below | A `refresh_token` is included only when one of the following is true: the request included the `offline_access` scope, or your `ClientApplication`'s registered grant types include `refresh_token`. For self-hosted, logins to a super admin project never receive a refresh token regardless of these settings. ### Using the SDK The `` React component handles both requests, PKCE, and the multi-factor/multiple-project branches automatically: ```tsx navigate('/')?.catch(console.error)}>

Sign in to Foo Medical

``` If you're not using React, call [`startLogin()`](/docs/sdk/core.medplumclient.startlogin) directly with the `MedplumClient` to perform request 1, handle the response cases described above, then call [`processCode()`](/docs/sdk/core.medplumclient.processcode) with the returned `code` to complete request 2 and receive your tokens. ### See Also - [Using Medplum as IDP](/docs/auth/medplum-as-idp) — the full-redirect variant of this same flow - [Multi-Factor Authentication](/docs/auth/mfa) - [`/oauth2/token` API reference](/docs/api/oauth/token) --- ## External Identity Providers import ExampleCode from '!!raw-loader!@site/../examples/src/auth/external-identity-providers.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; Along with [Google Authentication](/docs/auth/google-auth), Medplum supports other external identity providers using the OAuth2 protocol such as Auth0 and AWS Cognito for end user authentication. This is sometimes known as "Federated Identities". ### IDP Routing Strategies When using external identity providers, you need to decide how to route users to the correct IDP. Medplum supports two primary strategies: #### 1. App-based Routing (Client ID) In this model, the Identity Provider is determined by the application the user is logging into. This is configured on the `ClientApplication` resource. - **How it works:** You configure specific IDP details (Authorize URL, Client ID, etc.) on your Medplum `ClientApplication`. - **Use Case:** You have a "Patient App" that uses Auth0 for all users, and a separate "Provider App" that uses Okta. - **Setup:** Follow the [Setup](#setup) instructions below to configure the IDP on your `ClientApplication`. #### 2. Domain-based Routing (Email Domain) In this model, the Identity Provider is determined by the user's email address. This is configured using [Domain Configuration](/docs/auth/domain-level-identity-providers). - **How it works:** You map an email domain (e.g., `hospital.com`) to an IDP. When a user enters their email, Medplum routes them to that IDP. - **Use Case:** You have enterprise customers who want their employees to log in with their corporate credentials (SSO), regardless of which app they are using. - **Setup:** See the [Domain-level Identity Providers](/docs/auth/domain-level-identity-providers) guide. ### Auth flow When an end user authenticates with your client-side web application, we will use the following authentication flow: ![External identity providers flow](./external-identity-providers.webp) [Click to enlarge](./external-identity-providers.webp) ### Example repo Medplum provides a minimal example application which demonstrates using Auth0 as an external identity provider. The example is approximately 100 lines of TypeScript code, and can be used as a starting point for any standard OAuth2/OpenID identity provider. > [https://github.com/medplum/medplum-client-external-idp-demo](https://github.com/medplum/medplum-client-external-idp-demo) ### Setup Setup your external authentication provider (Auth0, AWS Cognito, Okta, etc). Use "https://api.medplum.com/auth/external" as the "redirect URI" (also known as the "callback URL" in some systems). Also make sure that your provider accepts the `profile` OIDC scope. Note the following details: - Authorize URL - Token URL - UserInfo URL - Client ID - Client secret Setup your Medplum account: - [Register for a Medplum account](/docs/tutorials/register) - Create a `ClientApplication` - Set the "Redirect URI" to "http://localhost:8000/" - Add an external identity provider with the details from above :::info[Self-hosted server-scoped identity providers] The `IdentityProvider` resource is server-scoped. On self-hosted deployments, creating or editing server-scoped identity provider configuration requires super admin context. Project administrators can configure identity provider settings on project `ClientApplication` resources, but they cannot create server-scoped `IdentityProvider` resources unless they are operating as super admin. ::: ### Start the flow The [MedplumClient](/docs/sdk/core.medplumclient) TypeScript class provides a [`signInWithExternalAuth`](/docs/sdk/core.medplumclient.signinwithexternalauth) convenience method: {ExampleCode} ### Handle the code When the external identity provider flow redirects back to your web application, it will include a `code` query parameter. This code can be exchanged for a Medplum access token. The [MedplumClient](/docs/sdk/core.medplumclient) TypeScript class provides a [`processCode`](/docs/sdk/core.medplumclient.processcode) convenience method: {ExampleCode} After the code is processed, the Medplum access token will be stored in the browser's local storage. The `MedplumClient` will automatically use the access token for all subsequent API calls. ### FAQ #### AWS Cognito - (If using the Cognito's hosted UI) Set the "redirect uri" by navigating to: Amazon Cognito > User Pools > [Your pool] > [Your App Client] > Edit Hosted UI > Allowed callback URLs - By default, Cognito does not include the `profile` scope. You can add this navigating to: Amazon Cognito > User Pools > [Your pool] > [Your App Client] > Edit Hosted UI > OpenID Connect Scopes - To disable PKCE for Cognito login, do the following: - In your client application, pass in `false` for the `pkceEnabled` parameter of `signInWithExternalAuth`. ```ts medplum.signInWithExternalAuth( EXTERNAL_AUTHORIZE_URL, EXTERNAL_CLIENT_ID, EXTERNAL_REDIRECT_URI, { projectId: MEDPLUM_PROJECT_ID, clientId: MEDPLUM_CLIENT_ID, redirectUri: WEB_APP_REDIRECT_URI, }, false, ), ``` - In the Medplum App, navigate to your [ClientApplication](https://app.medplum.com/ClientApplication) > Edit and set "PKCE Optional" to `true` --- ## Google Authentication Medplum supports using Google Authentication as an external identity provider, as well as [other external identity providers](/docs/auth/external-identity-providers) using the OAuth2 protocol. Google Authentication allows users to log in to your application using their Google profile. By default, Medplum automatically syncs user profile data with each user login, thereby ensuring that changes made in the connection source are automatically updated in Medplum. Optionally, you can disable user profile data synchronization to allow for updating profile attributes from your application. ### Prerequisites Before you begin: - [Sign up for a Google Developer account.](https://console.developers.google.com/) - [Create a Google Project](https://support.google.com/googleapi/answer/6251787?ref_topic=7014522#zippy=%2Ccreate-a-project) ### Create Google OAuth consent screen Configure your OAuth consent screen and create credentials in the [Google Developer Console](https://console.cloud.google.com/apis/credentials/consent). While setting up your OAuth consent screen, use the following settings: | Field | Value to Provide | | ------------------ | ---------------- | | User Type | External | | Application Type | Select Public. | | Authorized domains | medplum.com | While setting up OAuth scopes, the minimum scopes required are: - "./docs/auth/userinfo.email" - "./docs/auth/userinfo.profile" - "openid" If your application requests sensitive OAuth scopes or uses a custom image, Google will limit it to 100 logins until the OAuth consent screen is verified. Verification can take several days. To learn more about sensitive scopes, see Google's OAuth 2.0 Scopes for Google APIs documentation. To pass verification you will need to prove ownership of all Authorized domains you specify on Google's OAuth consent screen. If you're using custom domains, use your custom domain instead of medplum.com. ### Create Google credentials Create Google OAuth Client Credentials using Google's [Setting up OAuth 2.0](https://support.google.com/googleapi/answer/6158849) documentation. While setting up your credentials, use the following settings: | Field | Value to Provide | | ----------------------------- | ---------------------------------- | | Application type | Web application | | Authorized JavaScript origins | https://YOUR_DOMAIN | | Authorized redirect URIs | https://YOUR_DOMAIN/login/callback | When you successfully create the OAuth client, you will receive a **Client ID** and **Client Secret**. Google will present you with the option to "Download JSON". Do this, and save the JSON file for next steps. You can initiate a login attempt using the Medplum Client with the [`startGoogleLogin`](/docs/sdk/core.medplumclient.startgooglelogin) convenience method. ### Add Google Client ID to your Project Go to the [sites](https://app.medplum.com/admin/sites) section of your admin console to set up your domain. #### Add Google Client ID to your SignInForm If using the Medplum React Component library, add your Google Client ID: ```tsx navigate('/')?.catch(console.error)} googleClientId={process.env.GOOGLE_CLIENT_ID}>

Sign in to Foo Medical

``` You may want to use environment variables. Check your build tool for instructions. For example, with Webpack: ```tsx navigate('/')?.catch(console.error)} googleClientId={process.env.GOOGLE_CLIENT_ID}>

Sign in to Foo Medical

``` #### Update app deployment with auth keys To add Google auth to a [`@medplum/app`](/docs/app) deployment, modify the corresponding configuration file: ```js { // ... "googleClientId": "", "recaptchaSiteKey": "" } ``` Then, [re-deploy the app](/docs/self-hosting/install-on-aws#deploy-the-app) using the CLI: ``` npx medplum aws update-app ``` #### Common Login Issues If a user is experiencing issues logging in through Google SSO, follow these troubleshooting steps: 1. Clear browser cache and cookies 2. Try logging in using a different browser 3. Ensure the system clock on the user's device is set to the correct time and time zone - Incorrect system time can cause authentication tokens to be invalidated - Authentication tokens have a 1-hour timeout window which is validated against the system time --- ## MFA Under the Hood: Routes & User Flows This page is a deep dive into how Multi-Factor Authentication works in Medplum: the underlying HTTP routes and the user flows that combine them. It is intended for developers building their **own** MFA management UI — those who cannot use Medplum's [`SignInForm`](https://storybook.medplum.com/?path=/story/medplum-auth-signinform--basic) component or the [Medplum App Security page](/docs/auth/mfa) and need to drive enrollment and challenge completion directly. If you can use `SignInForm` or the Security page, prefer those — they handle every flow below for you. See the [MFA overview](/docs/auth/mfa) for the high-level feature documentation. Medplum supports two MFA methods: - **`totp`** — A Time-based One-Time Password from an authenticator app (Google Authenticator, Authy, etc.). Enrolling means scanning a QR code; verifying means entering the 6-digit code the app generates. - **`email`** — A 6-digit code emailed to the user's address on demand. Verifying means entering the code from the email. Email codes are single-use and expire after a short window. A user may enroll in either method or both. The methods a project allows are controlled by the `allowedMfaMethods` project setting (a `Project.setting` entry) — a comma-delimited string (`"totp"`, `"email"`, or `"totp,email"`). When unset, only `totp` is offered. ### Route Overview The routes fall into two groups depending on what they authenticate against. **Login-time routes** operate on a pending `login` (the `login` id returned by the login endpoints) and do **not** require a bearer token. Use these while completing a sign-in: | Method & Path | Body | Purpose | |---|---|---| | `POST /auth/login` | `{ email, password, ... }` | Start a login. The response carries the MFA signal (see [Login response signals](#login-response-signals)). | | `GET /auth/login/:login` | — | Re-fetch the pending login's status (same MFA fields as the login response). | | `POST /auth/mfa/login-enroll` | `{ login, method?, token? }` | Enroll a `mfaRequired` user who is logging in for the first time. `method` defaults to `totp`. | | `POST /auth/mfa/send-email` | `{ login }` | Send (or resend) the email code during a login challenge. Requires the user to be enrolled in email MFA. | | `POST /auth/mfa/verify` | `{ login, token }` | Complete the MFA challenge. `token` may be a TOTP code **or** an emailed code, whichever the user is enrolled in. | **Settings routes** operate on the **currently authenticated user** and require a bearer token. Use these to build a security-settings page where a logged-in user manages their factors: | Method & Path | Body | Purpose | |---|---|---| | `GET /auth/mfa/status` | — | Read the user's current enrollment status and the TOTP QR code while TOTP is not enrolled. | | `POST /auth/mfa/send-email-challenge` | `{}` | Email a 6-digit code to the current user (used to prove control of their email when enrolling email MFA, removing a factor, or disabling MFA). | | `POST /auth/mfa/enroll` | `{ method?, token }` | Enroll the current user in a method. `method` defaults to `totp`. | | `POST /auth/mfa/disable` | `{ token, method? }` | Remove a single factor (when `method` is given) or disable MFA entirely (when `method` is omitted). | And the [admin reset route](/docs/auth/mfa#admin-mfa-reset), `POST /admin/projects/:projectId/members/:membershipId/mfa/reset`, for project admins. ### Login response signals `POST /auth/login` (and `GET /auth/login/:login`) returns one of several shapes. Inspect these fields to decide which MFA step, if any, is required: | Field | Type | Meaning | |---|---|---| | `login` | string | The pending login id. Pass this to all login-time MFA routes. | | `mfaEnrollRequired` | boolean | The user must enroll in MFA before continuing (their account has `mfaRequired` but they are not yet enrolled). | | `enrollQrCode` | string | A data-URL QR code image for TOTP enrollment (present when enrollment is required/available). | | `enrollUri` | string | The raw `otpauth://` URI behind the QR code, for clients that render their own QR. | | `allowedMfaMethods` | `('totp' \| 'email')[]` | Which methods the project allows the user to enroll in. | | `mfaRequired` | boolean | The user is enrolled and must complete an MFA challenge before continuing. | | `mfaMethods` | `('totp' \| 'email')[]` | The methods the user is enrolled in (drives which prompts to show). | | `email` | string | The user's email, so the UI can show where an email code was sent. | | `code` | string | The OAuth authorization code — present once login (and any MFA) is complete. Exchange it for tokens. | | `memberships` | ProjectMembership[] | Present when the user must choose a profile before a `code` is issued. | :::note[] MFA is only enforced for **Username/Password** logins. Google and external-IdP logins are not challenged. When `email` is the user's **only** enrolled method, the login endpoint sends the email code automatically — you do not need to call `/auth/mfa/send-email` before showing the code-entry field. When the user is enrolled in `totp` (alone or alongside email), no code is sent automatically. ::: ### Flow: Completing an MFA challenge at login After `POST /auth/login` returns `mfaRequired: true`: ``` POST /auth/login ──▶ { login, mfaRequired: true, mfaMethods, email } │ ├─ totp enrolled: prompt for the authenticator code │ ├─ email only: code already sent; prompt for it │ (offer "Resend" → POST /auth/mfa/send-email { login }) │ └─ both enrolled: let the user choose; if they pick email, POST /auth/mfa/send-email { login } first │ ▼ POST /auth/mfa/verify { login, token } ──▶ { login, code, ... } │ ▼ Exchange `code` for tokens (or handle `memberships` if profile selection is required) ``` `token` in `/auth/mfa/verify` is accepted as either a TOTP code or an emailed code, so a single verify call works regardless of which factor the user chose. ### Flow: Forced enrollment at first login When a user was invited with `mfaRequired: true` but has not enrolled yet, `POST /auth/login` returns `mfaEnrollRequired: true`: ``` POST /auth/login ──▶ { login, mfaEnrollRequired: true, enrollQrCode, enrollUri, allowedMfaMethods } │ ├─ TOTP: show enrollQrCode; user scans and enters a code │ POST /auth/mfa/login-enroll { login, method: 'totp', token } │ ──▶ { login, code, ... } (enrollment + challenge done in one call) │ └─ Email: POST /auth/mfa/login-enroll { login, method: 'email' } ──▶ { login, mfaRequired: true, mfaMethods: ['email'], email } (server emails a code; the user must now verify it) POST /auth/mfa/verify { login, token } ──▶ { login, code, ... } ``` For TOTP, `login-enroll` both enrolls the user and verifies the supplied code, completing the login in one step. For email, enrollment makes email the user's factor and forces a normal email challenge (so the emailed code reverifies their address) before the login is granted. ### Flow: Self-enrolling from a settings page These flows assume the user is already authenticated (you have a bearer token). The token must not be limited by SMART resource scopes. For users that are not scoped to a single project, the token must come from a direct sign-in, e.g. password or Google. Start by reading status: ```ts const status = await medplum.get('auth/mfa/status', { cache: 'no-cache' }); // { // enrolled: boolean, // enrolledMethods: ('totp' | 'email')[], // allowedMethods: ('totp' | 'email')[], // mfaRequired: boolean, // if true, do not offer "Disable MFA" // email: string, // enrollUri?: string, // otpauth:// URI, only while TOTP is not enrolled // enrollQrCode?: string, // data-URL QR image, only while TOTP is not enrolled // } ``` `/auth/mfa/status` returns a TOTP `enrollQrCode`/`enrollUri` while the user is not enrolled in TOTP (generating a secret if the user does not have one yet), so you can offer authenticator enrollment at any time — including adding TOTP to an account that currently only has email. Once TOTP is enrolled, they are no longer returned. **Enroll in TOTP (authenticator app):** ``` GET /auth/mfa/status ──▶ show enrollQrCode │ user scans the QR and enters the generated code ▼ POST /auth/mfa/enroll { method: 'totp', token } ──▶ 200 OK ``` **Enroll in email-based MFA:** ``` POST /auth/mfa/send-email-challenge {} ──▶ emails a 6-digit code │ user enters the code from the email ▼ POST /auth/mfa/enroll { method: 'email', token } ──▶ 200 OK ``` Email enrollment requires the user to enter the emailed code, which reverifies that they currently control the address (and sets `emailVerified`). Both `/enroll` calls reject a method the user is already enrolled in, or a method the project does not allow. **Adding a second method:** the same calls work when the user is already enrolled. `enrolledMethods` from `/auth/mfa/status` tells you which methods are already active so you can offer only the missing one. ### Flow: Removing a factor or disabling MFA Both use `POST /auth/mfa/disable` and require a `token` proving control of one of the user's currently-connected factors. The accepted `token` is a TOTP code or an emailed code, depending on what the user is enrolled in — so if the user's only factor is email, email a code first with `POST /auth/mfa/send-email-challenge {}`. **Remove a single factor** (when more than one is enrolled), leaving the others in place: ``` POST /auth/mfa/disable { method: 'totp', token } ──▶ 200 OK ``` **Disable MFA entirely** (omit `method`): ``` POST /auth/mfa/disable { token } ──▶ 200 OK ``` Notes: - Removing `totp` (or disabling entirely) rotates the user's authenticator secret, so the old authenticator-app entry cannot be reused on a later re-enrollment. - An account with `mfaRequired` cannot remove its **last** factor. To rotate factors on such an account, enroll the replacement first, then remove the old one. ### Error responses The MFA routes return a FHIR `OperationOutcome` with `400 Bad Request` on failure. Common cases include `Invalid token` / `Invalid MFA token`, `MFA code expired`, `Already enrolled`, `MFA method not allowed`, `User not enrolled in MFA`, `Missing token`, and `Cannot remove the last MFA factor because MFA is required`. The login-time routes additionally reject a `login` that is already `revoked`, `granted`, or verified. --- ## Authentication :::tip[Choosing an authentication approach?] The [Authentication Decision Guide](/docs/decision-guides/authentication) helps you compare identity providers, account matching, and token-flow architectures. ::: This page helps you choose the correct authentication method for your application, whether it's a browser, a server, or a device. The primary goal is to guide you to the right documentation page based on your use case. For **user-facing applications (web or mobile apps)**, use [_browser-based authentication_](#browser-based-authentication). :::caution[Patient Login] The Medplum App (https://app.medplum.com) is an administrative tool for clinical users and developers. Patients **cannot** log in to this application directly. To provide patient access, you must build a custom patient portal using the Medplum SDK. ::: For applications running in a **trusted, back-end environment**, use [_server-side authentication_](#server-side-authentication). ### Browser-based Authentication This category is for user-facing applications that connect directly to Medplum. These applications run in untrusted environments (browsers, native apps) and use **identity providers** to authenticate a user. | Method | **Choose to...** | | :------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | | [Medplum as IDP](/docs/auth/medplum-as-idp) | (default) **Get going fast**, and don't have external compliance requirements. | | [External IDP](/docs/auth/external-identity-providers.mdx) | Connect to an external IDP, like [Google Auth](/docs/auth/google-auth), Auth0, or AWS Cognito. | | [Domain-level IDP](/docs/auth/domain-level-identity-providers) | Use your enterprise, domain-level **corporate identity solution.** | | [Direct External Auth](/docs/auth/direct-external-auth) | Authenticate directly with an external IDP token, without a token exchange flow (self-hosted only). | #### Mobile activation before account creation Some mobile apps need to verify an activation code before the patient or user account exists. For example, a clinic may create a `Patient`, send an activation code, and ask the patient to finish account setup in an iOS or Android app. Do not embed a long-lived `client_secret` in a mobile binary. Treat native mobile apps as public clients. Use a short-lived, narrowly scoped activation credential that can only call the activation workflow. The activation workflow should validate the code, expiration, intended patient, and retry limits before creating or linking the Medplum user account. After activation, use the normal authentication flow for future sessions. ### Server-side Authentication This category is for back-end services. These applications do not directly authenticate a user but operate on behalf of an application or device. They can be used to: - integrate with legacy systems - connect to a medical device - build a proxy API - connect to a CI/CD pipeline Use the [**Client Credentials Flow**](/docs/auth/client-credentials) to allow your application to use its own client key and secret to obtain a token. When working through proxy API layers, you can [enable on-behalf-of authentication](/docs/auth/on-behalf-of) with client credentials to act on behalf of other entities. For issuer-initiated workflows such as wallet handoff or magic links, use [**Pre-Authorized Code**](/docs/auth/pre-authorized-code) to mint a one-time code and redeem it at the token endpoint without an authorization redirect. --- ### User Management See our [User Management](/docs/user-management/) section for more information: - scoping users, and distinguishing [Project vs Server Scoped users](/docs/user-management/project-vs-server-scoped-users.mdx) - enabling [Open Patient Registration](/docs/user-management/open-patient-registration) - using [External ID's](/docs/user-management/external-ids) - creating [Custom Emails](/docs/user-management/custom-emails.mdx) for user invites --- ### Security Best Practices Regardless of the authentication method you choose, always follow these best practices: - **Principle of Least Privilege**: Ensure your `ClientApplication` and user access policies are as restrictive as possible. - **Secrets Management**: Never store credentials or tokens directly on disk. Use a secrets management service. - **Network Isolation**: Restrict host access via a VPC or firewall to prevent access from the public internet. --- ## Logout There are two different methods to "logout" and revoke access tokens: 1. Revoke current access token using `/oauth2/logout` 2. Revoke other session access token using `/auth/revoke` ### Revoke current access token Logout of the current session by revoking the current access token using `/oauth2/logout` endpoint. Send an HTTP POST request ```bash curl 'https://api.medplum.com/oauth2/logout' \ -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -d '{}' ``` Or use `MedplumClient`: ```ts await medplum.signOut(); ``` ### Revoke other session access token Users can retrieve a list of active sessions using the `/auth/me` endpoint. It will include basic session details such as User Agent and Remote Host (IP Address). Each session has a login `id`. You can use this login `id` to revoke a specific session access token. Users who signed in with a password or Google can list and revoke their sessions in all of their projects. Other logins can only list and revoke sessions in the current project. ```bash curl 'https://api.medplum.com/auth/revoke' \ -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer MY_ACCESS_TOKEN" \ -d '{"loginId":"MY_LOGIN_ID"}' ``` Or use `MedplumClient`: ```ts await medplum.post('/auth/revoke', { loginId: 'MY_LOGIN_ID' }); ``` --- ## Using Medplum as IDP ### Introduction When building your own healthcare App, you can use Medplum as an **identity provider** using the OAuth2 Authorization Code flow. Medplum provides OAuth2 endpoints to authenticate Patients and Practitioners and share data with your application. This guide will go over the basics of Medplum's OAuth2 authorization code flow, and you can find more detailed documentation in the [API Section](/docs/api/oauth/). We also have a **[minimal demo on Github](https://github.com/medplum/medplum-oauth-demo)** that implements a simple authorization code login. ### Create a Client Application The first step is to configure a [Medplum Client Application](https://app.medplum.com/ClientApplication) for your application on the Medplum Server. 1. Go to the [**Project Admin clients page**](https://app.medplum.com/admin/clients), either by clicking "Project" on the left sidebar, or navigating to https://app.medplum.com/admin/project 2. Click "Create New Client" to create your new ClientApplication resource 3. Set the `Redirect URI` to the URL to where you would like Medplum to redirect the user after login. 4. **(Optional but recommended)** Assign an [Access Policy](/docs/access/access-policies) to your client to restrict the data your application has access to ### Authorize your client Next your application will need to make an `HTTPS GET` request to the Medplum Server's [`/oauth2/authorize`](/docs/api/oauth/authorize) endpoint with the following URL parameters: | Parameter | Value | | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`client_id`](/docs/api/oauth/authorize#client_id-required) | The ID of your new Client Application | | [`redirect_uri`](/docs/api/oauth/authorize#redirect_uri-required) | **Must match exactly** to the uri registered in the Medplum App (including trailing slashes, punctuation, etc.), as detailed in the [OAuth2 Spec](https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2.3) | | [`response_type`](/docs/api/oauth/authorize#response_type-required) | Fixed value: `code` | | [`scope`](/docs/api/oauth/authorize#scope-required) | Fixed value: `openid` | See the [`oauth/authorize` API documentation](/docs/api/oauth/authorize) for more details about the possible request parameters. ### Fetch your token After your user successfully authenticates, they will get an [HTTP redirect response](/docs/api/oauth/authorize#authorization-code-grant) that is similar to this: ``` HTTP/1.1 302 Found Location: https://YOUR_APP/redirect_uri?code=AUTHORIZATION_CODE ``` The last step of this flow is for your application to trade in the `AUTHORIZATION_CODE` for an access token. 1. Read the `code` parameter from the query string to get the `AUTHORIZATION_CODE` 2. Call the [`oauth/token` endpoint](/docs/api/oauth/token) with the body parameters (details: [API Docs](/docs/api/oauth/token#request-parameters-in-body)) - `grant_type=authorization_code` - `client_id=YOUR_CLIENT_ID` - `code=AUTHORIZATION_CODE` 3. Use the `access_token` received in the [response](/docs/api/oauth/token#sample-response) to make future API calls (See the [**Client Credentials tutorial**](/docs/auth/client-credentials) for more details) :::note[Using the Medplum Client] You can also use the Medplum SDK to initiate this process with [`signInWithRedirect`](/docs/sdk/core.medplumclient.signinwithredirect). If you'd rather render the login form inside your own application instead of redirecting away from it, see [Embedded Login](/docs/auth/embedded-login), which uses [`startLogin`](/docs/sdk/core.medplumclient.startlogin) or `` instead. ::: ### See Also - [Authorize endpoint](/docs/api/oauth/authorize) - [Token endpoint](/docs/api/oauth/token) - [UserInfo endpoint](/docs/api/oauth/userinfo) --- ## Multi-Factor Authentication (MFA) import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Multi-Factor Authentication (MFA) adds an extra layer of security to user accounts by requiring a second authentication factor beyond a password. Medplum supports two MFA methods: - **Authenticator app (TOTP)** — a Time-based One-Time Password compatible with authenticator apps like Google Authenticator, Microsoft Authenticator, Authy, and others. - **Email** — a single-use 6-digit code emailed to the user's address each time it is needed. By default, only the authenticator app (TOTP) method is offered. Email-based MFA must be enabled per [`Project`](/docs/api/fhir/medplum/project) via the [`allowedMfaMethods`](#configuring-allowed-mfa-methods) setting. Users may enroll in more than one method and choose which to use at login. :::note[] Users enrolled in MFA will only be asked for an MFA code during a login with Username/Password ::: ### Configuring allowed MFA methods The MFA methods that users in a Project can enroll in are controlled by the `allowedMfaMethods` setting on the [`Project`](/docs/api/fhir/medplum/project) resource. It is stored as a single [`Project.setting`](/docs/self-hosting/project-settings) entry whose `valueString` is a comma-delimited list of method codes: | Value | Methods offered | | ------------- | ---------------------------------------------------------------- | | _(unset)_ | Authenticator app (TOTP) only — the historical default | | `totp` | Authenticator app (TOTP) only | | `email` | Email codes only | | `totp,email` | Both — users choose which to enroll in and which to use at login | When the setting is missing, empty, or contains no recognized value, Medplum falls back to `totp` only. #### Enabling email-based MFA To allow users in a Project to use email-based MFA, set the `allowedMfaMethods` setting to include `email`. `Project` settings can be edited by a Project Admin, or in the Medplum App by a Super Admin on the Project edit page. **TypeScript** ```ts const project = await medplum.readResource('Project', projectId); await medplum.updateResource({ ...project, setting: [ // Preserve any other settings, replacing allowedMfaMethods if it exists ...(project.setting ?? []).filter((s) => s.name !== 'allowedMfaMethods'), { name: 'allowedMfaMethods', valueString: 'totp,email' }, ], }); ``` **CLI** ```bash # Add the setting to the Project resource (replace and merge with # any existing settings) medplum patch Project/ \ '[{ "op": "add", "path": "/setting/-", "value": { "name": "allowedMfaMethods", "valueString": "totp,email" } }]' ``` Use `valueString: 'email'` to offer email codes only, or `valueString: 'totp'` (or remove the setting) to restrict users to authenticator apps. :::note[] Changing `allowedMfaMethods` affects which methods users can **newly enroll** in. Users already enrolled in a method that is later disallowed keep that method until they remove it. ::: ### Branding MFA emails and authenticator apps By default, MFA content names Medplum. A Project can white-label it with an `appName` [`Project.setting`](/docs/self-hosting/project-settings) entry — the server-side counterpart to the app's build-time [`MEDPLUM_APP_NAME`](/docs/self-hosting/branding), which the server cannot read — and it changes: - **Emailed codes** — the subject and body read "Your Acme Health verification code", signed "The Acme Health Team". - **Authenticator app entries** — enrollment QR codes use the app name as the TOTP issuer, so the entry the user scans into Google Authenticator (or similar) is titled `Acme Health` rather than `medplum.com`, listed under the user's email address: ``` Acme Health alice@example.com 123 456 ``` **TypeScript** ```ts const project = await medplum.readResource('Project', projectId); await medplum.updateResource({ ...project, setting: [ // Preserve any other settings, replacing appName if it exists ...(project.setting ?? []).filter((s) => s.name !== 'appName'), { name: 'appName', valueString: 'Acme Health' }, ], }); ``` **CLI** ```bash medplum patch Project/ \ '[{ "op": "add", "path": "/setting/-", "value": { "name": "appName", "valueString": "Acme Health" } }]' ``` When `appName` is missing or blank, emails and authenticator entries keep the Medplum defaults. Colons are removed from the app name before it is used as the TOTP issuer, because authenticator apps treat a colon as the separator between the issuer and the account name. With [Project SMTP](/docs/user-management/project-smtp) configured, `appName` also becomes the From display name, so the message shows as "Acme Health" in the recipient's inbox rather than a bare address. It is not applied to the server's own sender, because a display name that disagrees with the sender domain is a signal mail clients treat as suspicious. :::note[] `appName` applies when content is generated, so it only affects new authenticator enrollments. Entries already added to a user's authenticator app keep their original title, and changing the setting never invalidates an existing secret. ::: :::note[] `appName` currently covers MFA content and the email sender name. Welcome, password reset, and invite emails still name Medplum — use [Custom Emails](/docs/user-management/custom-emails) to replace those. The login page is branded separately, per client, via [`ClientApplication.signInForm`](/docs/api/fhir/medplum/clientapplication). ::: ### Self-Enrollment Users can self-enroll in MFA through the Medplum App security settings. The methods offered depend on the Project's [`allowedMfaMethods`](#configuring-allowed-mfa-methods) setting. #### Steps to Self-Enroll 1. Navigate to the Security page at `https://app.medplum.com/security` 2. You will see the "Multi Factor Auth" section showing your current enrollment status 3. Choose a method to enroll in: **Authenticator app (TOTP)** ![MFA Enrollment Screen](./mfa-enrollment.png) - A QR code will be displayed that you can scan with your authenticator app - Enter the 6-digit code from your authenticator app to complete enrollment **Email** - Click **"Add email-based MFA"** — a 6-digit code is emailed to your account's address - Enter the code to verify control of your email and complete enrollment Once enrolled, you will be required to provide an MFA code during login. When both methods are allowed, a user can enroll in both and add or remove individual methods from the Security page. You can disable MFA at any time by clicking the "Disable MFA" button (you'll need to provide a current MFA code to disable it). ### Requiring MFA #### Requiring MFA for all Project users To require MFA for every user who signs in to a Project with a username and password, add an `mfaRequired` entry to [`Project.setting`](/docs/self-hosting/project-settings) with `valueBoolean: true`. This applies to both existing and new users. A user who has not yet enrolled in MFA will be required to enroll during their next password login before they can access the Project. **TypeScript** ```ts const project = await medplum.readResource('Project', projectId); await medplum.updateResource({ ...project, setting: [ // Preserve any other settings, replacing mfaRequired if it exists ...(project.setting ?? []).filter((s) => s.name !== 'mfaRequired'), { name: 'mfaRequired', valueBoolean: true }, ], }); ``` **CLI** ```bash # Add the setting to the Project resource (replace and merge with # any existing settings) medplum patch Project/ \ '[{ "op": "add", "path": "/setting/-", "value": { "name": "mfaRequired", "valueBoolean": true } }]' ``` The project setting and the per-user [`User.mfaRequired`](/docs/api/fhir/medplum/user) field are additive. MFA is required if either value is `true`; setting `User.mfaRequired` to `false` does not exempt that user from a project-wide requirement. Set the Project setting to `false` or remove it to stop requiring MFA project-wide. Users whose individual `User.mfaRequired` field remains `true` will still be required to use MFA. :::note[] Medplum MFA is enforced only for username-and-password login. Authentication requirements for users who sign in through an external identity provider must be configured with that provider. ::: #### Requiring MFA for individual new users Administrators can require new users to set up MFA during the invitation process by setting the `mfaRequired` parameter to `true` in the [invite request](/docs/api/project-admin/invite). When a user is invited with `mfaRequired: true`: 1. A MFA secret is automatically generated for the user 2. During their first login, after entering their password, they will be prompted to enroll in MFA 3. They must complete MFA enrollment before they can access the system #### Example: Inviting a User with MFA Required **TypeScript** ```ts await medplum.invite(123, { resourceType: 'Practitioner', firstName: 'Jane', lastName: 'Doe', email: 'jane.doe@example.com', mfaRequired: true, }); ``` **CLI** ```bash medplum post admin/projects/:projectId/invite \ '{ "resourceType": "Practitioner", "firstName": "Jane", "lastName": "Doe", "email": "jane.doe@example.com", "mfaRequired": true }' ``` **cURL** ```bash curl https://api.medplum.com/admin/projects/:projectId/invite \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "resourceType": "Practitioner", "firstName": "Jane", "lastName": "Doe", "email": "jane.doe@example.com", "mfaRequired": true }' ``` For more details on the invite endpoint, see the [Invite User Endpoint](/docs/api/project-admin/invite) documentation. ### Admin MFA Reset Project admins can reset MFA for members who have lost access to a factor via the `POST /admin/projects/:projectId/members/:membershipId/mfa/reset` endpoint. In the Medplum App, this is available from the **Account Security** section of a member's detail page (**Admin → Users → _member_**), and as a bulk action on the users table. The request body accepts an optional `method` field: | `method` | Effect | | ----------- | --------------------------------------------------------------- | | _(omitted)_ | Resets `totp` — the backwards-compatible default | | `totp` | Resets the authenticator app factor and rotates the TOTP secret | | `email` | Resets the email factor; the TOTP secret is left untouched | Only the selected factor is reset; any other enrolled factors remain active. When reset: - The selected method is removed from the user's enrolled methods (`mfaMethod`), and `mfaEnrolled` is cleared once no factors remain. - Resetting `totp` rotates the TOTP secret, so the old authenticator app entry cannot be reused. - The user receives an email notification. - If no factors remain, the user must re-enroll in MFA on their next login (if `mfaRequired` is set) or via the Security settings page. The endpoint returns `400` if the member is not enrolled in the requested method. Unlike self-service MFA disable, an admin reset does not require an MFA code and can remove a required user's last factor (forcing re-enrollment at next login). **TypeScript** ```ts // Reset the authenticator app (TOTP) factor — the default await medplum.post(`admin/projects/${projectId}/members/${membershipId}/mfa/reset`, { method: 'totp' }); // Reset the email factor instead await medplum.post(`admin/projects/${projectId}/members/${membershipId}/mfa/reset`, { method: 'email' }); ``` **CLI** ```bash # Reset TOTP (default) medplum post admin/projects/:projectId/members/:membershipId/mfa/reset '{}' # Reset the email factor medplum post admin/projects/:projectId/members/:membershipId/mfa/reset '{"method":"email"}' ``` **cURL** ```bash curl -X POST https://api.medplum.com/admin/projects/:projectId/members/:membershipId/mfa/reset \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"method":"totp"}' ``` ### Admin Password Reset Project admins can send a member a password reset email via the `POST /admin/projects/:projectId/members/:membershipId/resetpassword` endpoint. This creates a single-use reset link and emails it to the member, mirroring the self-service reset flow but scoped to a known member. The member's current password remains valid until they complete the reset. This action is also available from the **Account Security** section of a member's detail page and as a bulk action on the users table. **TypeScript** ```ts await medplum.post(`admin/projects/${projectId}/members/${membershipId}/resetpassword`, {}); ``` **CLI** ```bash medplum post admin/projects/:projectId/members/:membershipId/resetpassword '{}' ``` **cURL** ```bash curl -X POST https://api.medplum.com/admin/projects/:projectId/members/:membershipId/resetpassword \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` To set a member's password directly (without emailing them), use the `POST /admin/projects/setpassword` endpoint with the member's email — also surfaced as **Set password** in the Account Security section. ### How email-based MFA works Unlike the authenticator app method, which derives codes from a shared secret stored on the user, email-based MFA issues a fresh single-use code each time one is needed. When a code is required, Medplum: 1. Generates a random 6-digit code. 2. Stores a hash of the code (never the code itself) on the user's `Login`, along with an expiration timestamp. 3. Emails the code to the user's address with the subject `Your Medplum verification code: `. The code is **valid for 20 minutes**. Each code is single-use — it is cleared once verified — and submitting an expired or incorrect code is rejected, so the user must request a new one. #### At login When a user enrolled in email-based MFA signs in with their username and password: 1. After the password is accepted, Medplum recognizes that the login still needs a second factor. 2. If email is the user's only enrolled method, the code is **sent automatically** and the sign-in form goes straight to the code-entry step. If the user also has an authenticator app, they can choose to receive a code by email instead. 3. The user enters the 6-digit code from their email to complete the login. If a code expires or is lost, the user can request a new one from the sign-in form, which sends a fresh code and resets the 20-minute window. #### At enrollment Enrolling in email-based MFA requires the user to prove control of their email address. When the user starts enrollment, Medplum emails a code; the user enters it to verify and finish enrolling. The same emailed-code verification is required when an enrolled user adds, removes, or disables an email factor. ### Using Medplum's SignInForm Component **We recommend using Medplum's [`SignInForm`](https://storybook.medplum.com/?path=/story/medplum-auth-signinform--basic) React component** for handling authentication flows that include MFA. The `SignInForm` component automatically handles all MFA-related UI and flows, including: - **MFA Enrollment**: Automatically detects when a user needs to enroll in MFA (e.g., when `mfaRequired: true` was set during invitation) and displays the enrollment screen with QR code - **MFA Verification**: Automatically prompts for MFA codes when users with enrolled MFA attempt to log in ### Building Your Own MFA UI Can't use `SignInForm` or the Medplum App Security page to manage MFA configuration and challenge completion? See [**MFA Under the Hood: Routes & User Flows**](./how-mfa-works.md) for the underlying routes and the enrollment and challenge flows you can drive directly. --- ## Mutual TLS (mTLS) import BrowserOnlyTabs from '@site/src/components/BrowserOnlyTabs'; import TabItem from '@theme/TabItem'; Mutual TLS (mTLS) is a client authentication mechanism built on top of standard TLS. In addition to the server presenting a certificate to the client (standard TLS), the client also presents an X.509 certificate to the server, which verifies it against a configured trust store. This provides strong, cryptographic proof of client identity without transmitting a shared secret. ### When to Use mTLS mTLS is an alternative to the standard [Client Credentials](./client-credentials) flow for machine-to-machine authentication. Consider mTLS when: - A trading partner or payer **requires** it — mTLS is an optional but increasingly expected part of the [Da Vinci Prior Authorization Support (PAS)](https://hl7.org/fhir/us/davinci-pas/privacy.html) specification (HTI-4 / electronic prior auth), and some payers are making it mandatory. - You want to eliminate shared secrets entirely and rely solely on PKI for client authentication. - Your security policy mandates certificate-based authentication for system integrations. Medplum's mTLS support follows [RFC 8705 — OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens](https://datatracker.ietf.org/doc/html/rfc8705). ### Prerequisites This tutorial assumes you already have a Medplum account. (If not, please [register](https://app.medplum.com/register).) You will need: 1. A [ClientApplication](https://app.medplum.com/ClientApplication) — create one on the [Project Admin page](https://app.medplum.com/admin/clients). 2. An X.509 client certificate (and its private key). This can be either: - A **self-signed certificate** — the certificate itself is added to the trust store. - A **CA-signed certificate** — the issuing CA certificate (or chain) is added to the trust store. 3. An mTLS-capable load balancer (AWS ALB recommended) or a reverse proxy configured to pass the client certificate to Medplum in the `x-amzn-mtls-clientcert` header (passthrough mode) or `x-amzn-mtls-clientcert-leaf` header (verify mode). See [Infrastructure Setup](#infrastructure-setup) below. ### Generating a Certificate If you do not already have a certificate, you can generate a self-signed one with OpenSSL: ```bash # Generate a private key openssl genrsa -out client.key 2048 # Generate a self-signed certificate (valid for 365 days) openssl req -new -x509 -key client.key -out client.crt -days 365 \ -subj "/CN=My Integration Client/O=My Organization" ``` For production use, issue the certificate from your organization's internal CA or a trusted third-party CA and add the CA certificate to the trust store rather than the leaf certificate. ### Configuring the ClientApplication Set the `certificateTrustStore` field on the `ClientApplication` to the PEM-encoded certificate(s) that are trusted to authenticate as this client. - For a **self-signed** certificate: paste the contents of `client.crt`. - For a **CA-signed** certificate: paste the CA certificate (or the full chain). Multiple PEM certificates can be concatenated in the same field. You can set this via the Medplum App UI (the field is on the ClientApplication edit page) or via the FHIR API: ```json { "resourceType": "ClientApplication", "name": "My Integration Client", "certificateTrustStore": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n" } ``` ### Obtaining a Token :::note[] mTLS authentication is only available on the dedicated mTLS endpoint: **`https://mtls.api.medplum.com`**. Requests to the standard `https://api.medplum.com` endpoint will not include a client certificate and cannot use mTLS authentication. ::: Once infrastructure is configured (see below), make a standard `client_credentials` token request — but **omit the `client_secret`**. The server authenticates the client via the certificate presented at the TLS layer. **cURL** ```bash curl -X POST https://mtls.api.medplum.com/oauth2/token \ --cert client.crt \ --key client.key \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials&client_id=$MY_CLIENT_ID" ``` **Python** ```python import requests def get_mtls_token(token_url, client_id, cert_path, key_path): headers = { "Content-Type": "application/x-www-form-urlencoded" } body = { "grant_type": "client_credentials", "client_id": client_id, } response = requests.post( token_url, data=body, headers=headers, cert=(cert_path, key_path), # mTLS client certificate ) return response.json()["access_token"] ``` On success, the response is a standard OAuth2 token response: ```json { "token_type": "Bearer", "access_token": "", "expires_in": 3600 } ``` The `access_token` value can then be used in subsequent API requests as a `Bearer` token. ### Infrastructure Setup Medplum does not terminate TLS itself — it relies on a TLS-terminating reverse proxy or load balancer to extract the client certificate and forward it as an HTTP header. The header name must be configured in the Medplum server configuration via `mtlsCertHeader`. #### AWS Application Load Balancer (Recommended) AWS ALB supports mTLS in two modes: | Mode | Header forwarded | `mtlsCertHeader` setting | | --------------------------- | ----------------------------- | ----------------------------- | | Passthrough | `x-amzn-mtls-clientcert` | `x-amzn-mtls-clientcert` | | Verify (ALB validates cert) | `x-amzn-mtls-clientcert-leaf` | `x-amzn-mtls-clientcert-leaf` | Medplum's CDK infrastructure supports a dedicated mTLS load balancer. Configure the following fields in your CDK stack configuration: ```json { "mtlsDomainName": "mtls.api.example.com", "mtlsSslCertArn": "arn:aws:acm:us-east-1:123456789012:certificate/...", "mtlsInternetFacing": true, "mtlsWafIpSetArn": "arn:aws:wafv2:us-east-1:123456789012:ipset/..." } ``` And in your Medplum server configuration (`medplum.config.json`): ```json { "mtlsCertHeader": "x-amzn-mtls-clientcert" } ``` #### nginx ```nginx server { listen 443 ssl; ssl_certificate /etc/ssl/server.crt; ssl_certificate_key /etc/ssl/server.key; # Request a client certificate but do not require it here — # Medplum validates it against the per-client trust store. ssl_verify_client optional_no_ca; location /oauth2/token { proxy_pass http://medplum-backend; proxy_set_header x-mtls-cert $ssl_client_escaped_cert; } } ``` And in your Medplum server configuration: ```json { "mtlsCertHeader": "x-mtls-cert" } ``` ### Certificate Trust Store Options The `certificateTrustStore` field accepts one or more PEM-encoded certificates concatenated together. Medplum supports: - **Self-signed certificates** — the exact certificate must be in the trust store. - **CA-signed certificates** — any certificate signed by a CA in the trust store is accepted. You can include multiple CA certificates to trust certificates from more than one issuer. ``` -----BEGIN CERTIFICATE----- -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- -----END CERTIFICATE----- ``` ### Further Reading - [RFC 8705 — OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens](https://datatracker.ietf.org/doc/html/rfc8705) - [Da Vinci PAS — Privacy, Safety, and Security](https://hl7.org/fhir/us/davinci-pas/privacy.html) (HTI-4 / electronic prior auth mTLS requirements) - [Client Credentials Flow](./client-credentials) --- ## On-Behalf-Of import ExampleCode from '!!raw-loader!@site/..//examples/src/auth/on-behalf-of.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; The Medplum API supports an "On-Behalf-Of" feature to enable a Customer Server Side App to act on behalf of a Medplum user. This feature is designed to enable a common Medplum architectural pattern where the Customer Server Side App is the only system component that interacts with Medplum Server. ### Overview Consider this common Medplum architectural pattern: ![On-Behalf-Of](./on-behalf-of.webp) Here, the Medplum "Customer" has a custom End User App and a Server Side App to enable their business requirements. In this model, the Customer Server Side App is the only system component that interacts with Medplum Server. In this model, the Medplum Server is more like an internal data store. Historically, it was difficult to use this model and take advantage of many Medplum features related to Medplum authentication (author tracking, access policies, etc). Now, with the "On-Behalf-Of" feature, the Customer Server Side App can act on behalf of a Medplum user. ### How to Use First, the Customer Server Side App must authenticate with Medplum Server as a `ClientApplication`. This is the same as any other Medplum `ClientApplication`. The only requirement for the `ClientApplication` is that it has [Project Admin](/docs/access/admin#project-admin) rights. This is because the `ClientApplication` will be acting on behalf of a Medplum ProjectMembership. Then, when making API requests, the Customer Server Side App must include an `X-Medplum-On-Behalf-Of` HTTP header. The value of this header can be either: 1. A `ProjectMembership` ID - This is the most direct and explicit way to specify the Medplum user. The Customer Server Side App would presumably store this id in its own database. 2. A profile ID such as `Patient` or `Practitioner` - This is a more natural value for the Customer Server Side App to use, but requires a lookup to find the corresponding `ProjectMembership`. For more information on `ProjectMemberships` and profile resources, see the [User Management Guide](/docs/user-management). The `X-Medplum-On-Behalf-Of` header is only valid for the current request. It is not stored or persisted in any way. The `X-Medplum-On-Behalf-Of` feature is compatible with both JWT access tokens and Basic Authentication. ### How it Works When the Medplum Server receives a request with the `X-Medplum-On-Behalf-Of` header, it will: 1. Authenticate the `ClientApplication` as normal 2. Check for the `X-Medplum-On-Behalf-Of` header 3. If present, set up the Repository instance accordingly - Set `onBehalfOf` to the specified `ProjectMembership` ID - Use the `ProjectMembership` access policy 4. Proceed with the request as normal 5. Include the `onBehalfOf` value in the `meta.onBehalfOf` element of the response ### Example Here is a curl example of using the `X-Medplum-On-Behalf-Of` header: {ExampleCode} Here is the same example using the Medplum SDK: {ExampleCode} If you want to apply the header to all requests, set a default header when constructing the client: {ExampleCode} Note the two extra HTTP request headers: 1. `X-Medplum: extended` - This is required to enable the extended Medplum API features, such as `meta.author` and `meta.onBehalfOf`. 2. `X-Medplum-On-Behalf-Of` - This is the `ProjectMembership` ID that the client is acting on behalf of. Here is the resulting FHIR JSON: ```json { "resourceType": "Patient", "id": "00000000-689b-4e20-a3c2-6f55108b432a", "meta": { "versionId": "00000000-b58b-47c8-9fc0-f85627d333c9", "lastUpdated": "2024-07-13T21:18:45.850Z", "author": { "reference": "ClientApplication/00000000-d361-46f0-adf4-f56da467dc08", "display": "My Client" }, "onBehalfOf": { "reference": "Practitioner/00000000-9886-4b83-a25a-3b99563b8127", "display": "My Test User" } }, "name": [{ "given": ["Homer"], "family": "Simpson" }] } ``` --- ## Pre-Authorized Code import ExampleCode from '!!raw-loader!@site/../examples/src/auth/pre-authorized-code.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Medplum supports the OAuth 2.0 pre-authorized code grant type, `urn:ietf:params:oauth:grant-type:pre-authorized_code`, as defined by [OpenID for Verifiable Credential Issuance 1.0](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html). This flow is useful for magic links and other issuer-initiated flows where user authentication and consent have already happened before the client asks Medplum for tokens. ### Overview This flow has two steps: 1. A trusted backend creates a one-time pre-authorized code by calling `/auth/preauthorize`. 1. The receiving application redeems that code at `/oauth2/token` to obtain Medplum tokens. Unlike the authorization code flow, this flow does not redirect the user through the authorization endpoint. ### Prerequisites To use this flow: - Create a [`ClientApplication`](https://app.medplum.com/ClientApplication) with [Project Admin](/docs/access/admin#project-admin) privileges. - Authenticate your backend as that `ClientApplication`, typically with the [Client Credentials Flow](/docs/auth/client-credentials). - Include the `X-Medplum-On-Behalf-Of` header when creating the pre-authorized code. This identifies the Medplum user who will receive the resulting token. See [On-Behalf-Of](/docs/auth/on-behalf-of) for details. :::caution[One-time codes] Pre-authorized codes are single-use and expire automatically. By default, Medplum issues codes that expire after 1 hour. The maximum allowed lifetime is 7 days. ::: ### Create a pre-authorized code Send an authenticated `POST` request to `/auth/preauthorize`. Request headers: - `Authorization`: Your `ClientApplication` access token or basic auth credentials - `X-Medplum-On-Behalf-Of`: The target `ProjectMembership`, `Practitioner`, `Patient`, or other supported profile reference - `Content-Type: application/json` Request body: - `clientId`: Required. The target [`ClientApplication`](https://app.medplum.com/ClientApplication) ID. - `scope`: Optional. See [Scope](#scope) for the default and the allowed values. - `nonce`: Optional. If omitted, Medplum generates a random nonce. - `expiresIn`: Optional expiration time in seconds. Must be between `1` and `604800`. Example: **TypeScript** {ExampleCode} **cURL** {ExampleCode} On success, Medplum returns a response like: ```json { "preAuthorizedCode": "", "expiresAt": "2026-05-16T20:15:00.000Z" } ``` You can package this code into a magic link, QR code, or handoff to a wallet or application that will redeem it. #### Scope The scope of a pre-authorized code can never be broader than the scope of the access token used to create it. If the calling access token has no [SMART scopes](/docs/access/smart-scopes), it is unrestricted, and `scope` defaults to `openid`. If the calling access token has SMART scopes, such as `user/*.rs`: - When `scope` is omitted, it defaults to `openid` plus the caller's SMART scopes. Other scopes, such as `offline_access`, are not inherited. - Each requested SMART scope must be covered by one of the caller's SMART scopes, with the same or fewer resource types and permissions. - A `system/` scope covers the same `user/` scope. A `system/` or `user/` scope covers the same `patient/` scope only when the target user's profile is a `Patient`, and the code is then limited to that patient's compartment. - `scope` must include at least one SMART scope. A scope with no SMART scopes, such as `openid` alone, would be unrestricted. - Any other requested scope, except `openid`, must also be in the caller's scope. For example, a caller with `openid user/*.rs` can create a code with `openid user/Patient.rs`, or with `openid patient/*.rs` for a `Patient` user, but not with `openid user/Patient.cruds`. If the requested scope is broader than the caller's scope, Medplum returns `400 Bad Request` with `Invalid scope`. ### Redeem the pre-authorized code To redeem the code, send a form-encoded `POST` request to `/oauth2/token` with: - `grant_type`: `urn:ietf:params:oauth:grant-type:pre-authorized_code` - `client_id`: The [`ClientApplication`](https://app.medplum.com/ClientApplication) ID - `pre-authorized_code`: The one-time code returned by `/auth/preauthorize` Example: **TypeScript** {ExampleCode} **cURL** {ExampleCode} On success, the response includes a Medplum access token and ID token: ```json { "token_type": "Bearer", "access_token": "", "id_token": "", "scope": "openid", "expires_in": 3600 } ``` ### Notes - Pre-authorized codes can only be redeemed once. - If a code is expired, already used, revoked, or presented for the wrong client, Medplum returns an OAuth error response from `/oauth2/token`. - Medplum currently supports the pre-authorized code flow without an additional transaction code (`tx_code`). For more details, see the [OpenID for Verifiable Credential Issuance 1.0 specification](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html), especially the sections on [Pre-Authorized Code Flow](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#section-3.5) and [Token Request](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#section-6.1). --- ## Access Token Management import ExampleCode from '!!raw-loader!@site/../examples/src/auth/session-management.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; This guide explains how Medplum manages authentication tokens and how to implement secure token handling in your applications. ### Access Tokens vs Refresh Tokens Medplum uses two types of tokens to manage authentication securely: **Access Tokens** are short-lived JSON Web Tokens (JWTs) that grant access to Medplum APIs. Each access token contains essential information about the user's permissions and session details. Every API request must include a valid access token. **Refresh Tokens** are long-lived credentials used to obtain new access tokens without requiring the user to log in again. These tokens are stored securely by client applications and are only transmitted when requesting a new access token. #### Requesting Refresh Tokens Refresh tokens are not issued by default. To receive a refresh token, you must include one of the following scopes in your authentication request: - `offline_access` - `offline` For example: {ExampleCode} {ExampleCode} ### Configuring Token Lifetimes #### Default Token Lifetimes | Token Type | Default Lifetime | | ---------- | ------------------------- | | Access | 3600 seconds (1 hour) | | Refresh | 1209600 seconds (2 weeks) | #### Customizing Token Lifetimes You can adjust token lifetimes by configuring your [`ClientApplication`](/docs/api/fhir/medplum/clientapplication) resource. Custom token lifetimes are useful when your application has specific security requirements or usage patterns. Here's how to modify token lifetimes: {ExampleCode} :::warning[] Consider your security requirements carefully when adjusting these values. Longer access token lifetimes increase convenience but also increase the window of vulnerability if a token is compromised. ::: ### Managing Token Refresh with Grace Periods The Medplum client includes a token refresh system that helps prevent service interruptions. The system uses a "grace period" to proactively refresh tokens before they expire. #### How Grace Periods Work The grace period represents how long before expiration the client should attempt to refresh the token. By default, this is set to 5 minutes (300,000 milliseconds). For example, if your access token expires at 2:00 PM, the client will attempt to refresh it starting at 1:55 PM. The Medplum client automatically handles token refresh in three scenarios: 1. Before making API requests when the token is within the grace period 2. When receiving a 401 Unauthorized response from the server 3. When your code explicitly requests a refresh :::warning[Grace Period Limitations] Note that the grace period mechanism only triggers in the above scenarios. This means that if your application is idle (not making any API calls), tokens can expire even with a configured grace period. The grace period does not create background refresh tasks. This is particularly important for mobile applications where the user may not interact with the app for extended periods of time. For example: 1. User logs in at 1:00 PM with a 1-hour access token (expires 2:00 PM) 2. Application is idle from 1:30 PM to 2:30 PM 3. At 2:30 PM when activity resumes, the token will have expired To handle this scenario, you should: - Check authentication status when resuming activity - Refresh the token if needed before making requests - Consider increasing the access token lifetime to reduce the frequency of token refreshes ::: #### Customizing the Grace Period You can adjust the grace period when initializing the Medplum client: {ExampleCode} ### Best Practices for Token Management 1. **Use Short-Lived Access Tokens** Keep access token lifetimes short (1-2 hours maximum) to minimize the impact of token theft. The Medplum client's automatic refresh mechanism means you don't need long-lived access tokens for continuous operation. 2. **Leverage Automatic Token Refresh** The Medplum client handles token refresh automatically. Configure the grace period appropriately for your application's needs: {ExampleCode} 3. **Token Revocation** If you suspect a token has been compromised, revoke it immediately using the `/auth/logout` endpoint. Monitor your audit logs for suspicious activity that might indicate compromised tokens. ### Conclusion For more advanced authentication scenarios, refer to our [Authentication reference](/docs/auth). --- ## Token Exchange import ExampleCode from '!!raw-loader!@site/../examples/src/auth/token-exchange.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ### Overview Medplum supports the [OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693) proposed standard. Token exchange provides a mechanism exchange an external access token for a Medplum access token without redirecting the user. This is useful when your application has already authenticated the user with an external identity provider, such as [Auth0](https://auth0.com/), and would access the medplum api services without requiring the user to log in again. ### Set up your ClientApplication In order to authenticate the user, Medplum's authentication server will call the `/userinfo` endpoint of the external identity provider and check for a valid response. Therefore, it is important that the corresponding [`ClientApplication`](https://app.medplum.com/ClientApplication) in your Medplum project has an identity provider set up with a user info URL specified. To set up an identity provider for your [`ClientApplication`](https://app.medplum.com/ClientApplication): 1. Log in to your Medplum project. 1. Navigate to the [ClientApplications page](https://app.medplum.com/ClientApplication). 1. Click on the [`ClientApplication`](https://app.medplum.com/ClientApplication) that you would like to set up with an identity provider. 1. Click the "Edit" tab. 1. Scroll down to the "Identity Provider" section. 1. Enter the `/userinfo` URL for your external identity provider in the "User Info URL" textbox. 1. Click "Save" to save your changes. :::caution[Scopes] By default, Medplum will use the user's email address to identify their Medplum user, so you must make sure that you have requested your external access token with the `email` scope. If you would like to use the `sub` (subject) identifier assigned by the external authentication provider, you must check "Use Subject" in your "Identity Provider" settings. ::: ### Exchanging tokens Once you have set up the identity provider for your [`ClientApplication`](https://app.medplum.com/ClientApplication), you can use the `/oauth2/token` endpoint to exchange the external access token for a Medplum access token. The client makes a token exchange request to the token endpoint with an extension grant type using the HTTP POST method. The following parameters are included in the HTTP request entity-body using the application/x-www-form-urlencoded format with a character encoding of UTF-8. - **grant_type**: The constant value of "urn:ietf:params:oauth:grant-type:token-exchange". - **client_id**: The selector for the external auth provider making the exchange request. This is either the ID of the [`ClientApplication`](https://app.medplum.com/ClientApplication) in your Medplum project, or the `clientId` of a server-level [`externalAuthProviders`](#server-scoped-users-and-the-membership_id-parameter-self-hosted-only) entry (self-hosted only). - **subject_token**: The access token that was generated by the external identity provider. - **subject_token_type**: The subject token type as defined in Section 3. Only "urn:ietf:params:oauth:token-type:access_token" is currently supported. - **membership_id** (optional): The [`ProjectMembership`](/docs/api/fhir/medplum/projectmembership) that the resulting Medplum token should be scoped to. Use it to disambiguate when the resolved user has more than one candidate membership. The set of candidates depends on the `client_id`: a `ClientApplication` constrains the candidates to its own project, while a server-level provider considers the user's memberships across all projects — see [Server-scoped users and the `membership_id` parameter](#server-scoped-users-and-the-membership_id-parameter-self-hosted-only) below. :::tip[Example] **TypeScript** {ExampleCode} **cURL** {ExampleCode} ::: The response will include a Medplum access token, which you can use to access the Medplum API. The Medplum SDK also provides the [`exchangeExternalAccessToken()`](/docs/sdk/core.medplumclient.exchangeexternalaccesstoken) helper method. ### Server-scoped users and the `membership_id` parameter (self hosted only) The setup above configures the external identity provider on a single [`ClientApplication`](https://app.medplum.com/ClientApplication). Because a `ClientApplication` lives inside one [`Project`](/docs/api/fhir/medplum/project), the exchange is implicitly scoped to that project: Medplum only considers the user's [`ProjectMembership`](/docs/api/fhir/medplum/projectmembership) records **within the client's project**. For most [project-scoped users](/docs/user-management/project-vs-server-scoped-users) that resolves to a single membership and the token "just works". In two situations there is more than one candidate membership, and you need `membership_id` to pick deterministically: - **Multiple memberships in the same project.** If the resolved user has more than one membership in the client's project, the exchange would otherwise fall back to the _first_ one. Pass `membership_id` to choose. This works with a plain `ClientApplication` — no server-level config required. - **Reaching a different project (server-scoped users).** [Server-scoped users](/docs/user-management/project-vs-server-scoped-users#server-scoped-users) (typically developers and administrators) can have a membership in _many_ projects. A `ClientApplication` cannot reach across projects — it only ever resolves into its own project — so to target an arbitrary project you must use a **server-level external auth provider** (below), where `membership_id` both selects the membership and derives the project. #### Configure a server-level external auth provider To support token exchange for server-scoped users, configure the identity provider at the server level via [`externalAuthProviders`](/docs/self-hosting/server-config) instead of (or in addition to) a per-`ClientApplication` identity provider. Unlike a `ClientApplication`, a server-level provider is not tied to any single project, so the exchange can resolve into whichever project you target. :::caution[Self-hosted only] `externalAuthProviders` is part of the [server config](/docs/self-hosting/server-config) and requires super admin access, so it is only available on self-hosted deployments. On Medplum's hosted service, token exchange is scoped to a `ClientApplication`'s project. If you need cross-project token exchange on hosted Medplum, contact [Medplum support](/contact). ::: ```json { "externalAuthProviders": [ { "issuer": "https://your-idp.example.com", "clientId": "your-token-exchange-selector", "userInfoUrl": "https://your-idp.example.com/oauth2/userinfo" } ] } ``` The `clientId` you assign here is the value you pass as `client_id` in the exchange request. (If you omit the top-level `clientId`, the server falls back to `identityProvider.clientId`.) Restart the server after updating the config. #### Why you must pass `membership_id` When you call the token endpoint with a server-level provider, Medplum validates the external token and resolves the user by email or `sub`. If you **do not** pass `membership_id`, the server falls back to the user's _first_ membership — which is non-deterministic for a server-scoped user that belongs to several projects, and may not be the project you intended. To pick the project explicitly, include `membership_id` in the request. The server reads that [`ProjectMembership`](/docs/api/fhir/medplum/projectmembership), derives the project from it, and issues a token scoped to that membership: ```bash curl -X POST 'https://your-medplum-server.example.com/oauth2/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ --data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \ --data-urlencode "client_id=your-token-exchange-selector" \ --data-urlencode "subject_token=" \ --data-urlencode "membership_id=" ``` With the SDK, pass it as the third argument to [`exchangeExternalAccessToken()`](/docs/sdk/core.medplumclient.exchangeexternalaccesstoken): ```ts await medplum.exchangeExternalAccessToken(externalAccessToken, clientId, membershipId); ``` #### You own the identity-to-membership mapping The external IdP token only carries the external identity (email or `sub`). It has no knowledge of Medplum projects or `ProjectMembership` ids. That means **your application is responsible for knowing which membership to target** for a given user and context, and for passing the right `membership_id` on each exchange: - **Keep track of memberships in your own system.** Persist the mapping from your external user (and the project/tenant context they're acting in) to the corresponding Medplum `ProjectMembership` id, so you can look it up at exchange time. - **Discover memberships when you need to.** You can enumerate a user's memberships by searching [`ProjectMembership`](/docs/api/fhir/medplum/projectmembership) resources (for example, by `profile` or `user`) with a sufficiently privileged token, then store the ids you care about. - **Validate the target.** An invalid or unreadable `membership_id` is rejected with an `invalid_request` error, so confirm the membership exists and belongs to the expected user/project before relying on it. :::tip[Example] The [server-side token exchange example](https://github.com/medplum/medplum/tree/main/examples/medplum-token-exchange-demo) demonstrates this end to end: it configures a server-level `externalAuthProviders` entry, obtains an external token, and exchanges it for a Medplum token while optionally targeting a specific `membership_id`. ::: ### Legacy `/auth/exchange` endpoint Before supporting the OAuth 2.0 Token Exchange standard, Medplum provided the `/auth/exchange` endpoint. This endpoint is deprecated, and developers are encouraged to adopt the standard. --- ## Creating CMS 1500 Medplum supports customizable creation of CMS 1500 for use in billing. The CMS 1500 form is a standardized form used in the United States to submit healthcare claims to Medicare and other health insurance providers. It is widely used by non-institutional providers and suppliers, such as physicians and outpatient clinics, to bill Medicare Part B and other insurers for services provided to patients. The CMS 1500 requires information about the patient, the medical services provided, and their costs. ### CMS 1500 and FHIR The data that fills the content of the CMS 1500 lives (largely) on the [Patient](/docs/api/fhir/resources/patient), [Coverage](/docs/api/fhir/resources/coverage), [Claim](/docs/api/fhir/resources/claim) and [Encounter](/docs/api/fhir/resources/encounter) FHIR resources. Your charting process should accurately populate these resources to ensure streamlined billing. ### Claim Export FHIR Operation To export a claim as PDF you can use the following operation ```bash curl 'https://api.medplum.com/fhir/R4/Claim//$export' \ -H 'Authorization: Bearer ' ``` --- ## Creating Superbills Medplum supports customizable creation of superbills. A superbill is a detailed form used by healthcare providers that outlines the services provided to a patient. It is typically given to patients after their medical appointments so they can submit it to their insurance companies for reimbursement. Superbills are important in settings where the provider does not directly bill the insurance company, such as in many out-of-network situations or when services are provided by a provider who operates on a cash-only basis. Similar to the generation of the [CMS 1500](/docs/billing/creating-cms1500) the Superbill is typically generated when an encounter is finished. The recommended implementaiton pattern is to create a Superbill as PDF and attaches it to a `Claim` resource. Superbills are typically not synchronized to external systems, but are sent to payors via patients or directly as a bill for out of network services. --- ## Billing :::tip[Planning this workflow?] The [RCM & Billing Decision Guide](/docs/decision-guides/rcm-billing) walks through requirements questions and FHIR modeling decisions for billing — use it alongside these docs. ::: The FHIR spec supports [many resources](/products/billing#fhir-resources) related to billing and payments. These resources can be created programmatically to support billing and payments. Commonly as part of a billing implementation, FHIR resources are created by applications and synchronized to clearinghouses or billing providers using [subscriptions](/docs/subscriptions) and [bots](/docs/bots/). For example, after a lab test is completed, a [DiagnosticReport](/docs/api/fhir/resources/diagnosticreport.mdx) is created, and it and related resources are automatically sent to a billing system after the report is finalized. For billing insurance, the [Coverage](/docs/api/fhir/resources/diagnosticreport.mdx) resource is critical for representing a patient's insurance. Refer to our [Patient Insurance](/docs/billing/patient-insurance) guide for more information on properly storing patient insurance information. ### Coding For resources to be billed appropriately, they often need to be tagged with CPT Codes, LOINC or SNOMED ontologies. To accomplish this, resources are often tagged with a [Codeable Concept](/docs/fhir-basics#standardizing-data-codeable-concepts). Coding will be determined by the service provided. Through automation and integration, more complex scenarios like determining authorization or checking whether insurance is active can be automated via bots. ### Sample Integrations The [medplum-demo-bots](https://github.com/medplum/medplum-demo-bots) Github repository has sample billing integrations that demonstrate how to maintain and synchronize billing data. - [Stripe](https://github.com/medplum/medplum/tree/main/examples/medplum-demo-bots/src/stripe-bots) integration shows how to keep invoices and payments synchronized between Medplum and Stripe. - [Candid Health](https://github.com/medplum/medplum/tree/main/examples/medplum-demo-bots/src/candid-health) integration shows how to prepare an Encounter resource and associated metadata for submission. --- ## Insurance Eligibility Checks import ExampleCode from '!!raw-loader!@site/..//examples/src/billing/insurance-eligibility-checks.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; Insurance eligibility checks determine whether a patient's insurance is active, in-network, and has applicable benefits. They ensure that the provider will ultimately get compensated by the patient's insurer for a specific product or service. ### Use Cases Insurance eligibility checks cover a variety of use cases, but generally they are used to answer three questions: 1. Is this insurance active? 2. Does this insurance cover basic visits to a provider? 3. Does this insurance cover a specific service type? The most basic use case for an eligibility check is simply seeing if the policy is active and in force. Adding a second layer is checking if the policy is active and also covers basic visits to a provider. These include appointments like physicals and check-ups and is the most common type of eligibility request. Because it is so common, these types of requests are defined by the [X12 Service Type Codes](https://x12.org/codes/service-type-codes) in **service code 30**. This service type is "Plan Coverage and General Benefits", and checks for active basic coverage. Two common use cases for this service type are: - Checking that a new patient's coverage is active and has general benefits. - Checking coverage directly prior to a visit to ensure that the policy has not changed and the patient is still covered. Finally, checking that insurance covers a specific service type adds a third layer to an eligibility check. These are done to ensure that a patient is covered for more complex care. It is recommended to use the [X12 Service Type Codes](https://x12.org/codes/service-type-codes) to illustrate the type of care that is being checked. For example, if a patient needs to purchase durable medical equipment, you would use **service code 12**. This code represents "Durable Medical Equipment Purchased". It is important to note that you should be checking the _service type_ to be provided, not the _specific service_, which is exactly what the [X12 Service Type Codes](https://x12.org/codes/service-type-codes) are designed to do. ### Preparing an Eligibility Check FHIR follows a request/response pattern for eligibility checks. This uses two resources: the [`CoverageEligibilityRequest`](/docs/api/fhir/resources/coverageeligibilityrequest) to model the request to the insurer and the [`CoverageEligibilityResponse`](/docs/api/fhir/resources/coverageeligibilityresponse) to model their response. To complete an eligibility check, you will need the following information: - Patient demographic info, modeled as a reference to a [`Patient`](/docs/api/fhir/resources/patient) resource. - Patient insurance coverage, modeled as a reference to a [`Coverage`](/docs/api/fhir/resources/coverage) resource (see our [insurance guide](/docs/billing/patient-insurance) for more info). - Provider info, modeled as a reference to the relevant [`Practitioner`](/docs/api/fhir/resources/practitioner), [`PractitionerRole`](/docs/api/fhir/resources/practitionerrole), or [`Organization`](/docs/api/fhir/resources/organization) (see the [provider network guide](/docs/administration/provider-directory/provider-networks) for more info). - The procedure/service to be provided and the diagnosis. This information should be on the relevant [`Encounter`](/docs/api/fhir/resources/encounter). ### Creating a Request FHIR provides the [`CoverageEligibilityRequest`](/docs/api/fhir/resources/coverageeligibilityrequest) resource to model a request for an insurance eligibility check. | **Element** | **Description** | **Code System** | **Example** | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------------------------------------------------- | | `patient` | A reference to the [`Patient`](/docs/api/fhir/resources/patient) the request is for. | | Patient/homer-simpson | | `provider` | A reference to the [`Practitioner`](/docs/api/fhir/resources/practitioner) or [`Organization`](/docs/api/fhir/resources/organization) that will be providing the service. This is the party that is submitting the request, but is not necessarily the specific [`Practitioner`](/docs/api/fhir/resources/practitioner) who will be providing the service detailed within. | | Practitioner/dr-alice-smith | | `insurer` | A reference to the insurance [`Organization`](/docs/api/fhir/resources/organization) that is providing coverage and will be evaluating the request. | | Organization/blue-cross | | `purpose` | The reason the request is being made. Must be one of the following: `auth-requirements`: A check of prior authorization that is required for the specified product or service. `benefits`: The benefits on the plan or the benefits consumed by the specified product or service. `discovery`: A request for the insurer to report any coverages they are aware of in addition to the ones specified. `validation`: A check that the specified coverages are in-force. | | validation | | `insurance.coverage` | A reference to the [`Coverage`](/docs/api/fhir/resources/coverage) resource that is being checked. | | Coverage/example-coverage | | `item` | Details about the items, services, or procedures for which eligibility is being checked. | | [See below](#the-item-being-checked-for-coverage) | | `status` | The status of the request. | | active | | `supportingInfo` | Additional information about the request. This could include a patient's condition, more details about the situation, special considerations, or more. | | | #### The Item Being Checked for Coverage As mentioned in the table above, the `item` element provides details about the eligibility being checked. This includes what procedure, product, or service is being provided as well as _why_ it is being provided. The `item` field also provides additional data about the procedure, product, or service. | **Property** | **Description** | **Code System** | **Example** | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------------- | | `category` | The general type of the service or product being checked for eligibility. | [X12 Codes](https://x12.org/codes/service-type-codes) | Vision Coverage | | `productOrService` | The product, drug, service, etc. that is being provided. | [CPT Codes](https://www.ama-assn.org/topics/cpt-codes) | 92340 - Fitting of eyeglasses | | `diagnosis` | The diagnosis for which care is being sought. | [ICD-10 Codes](https://www.icd10data.com/ICD10CM/Codes) | Condition/reduced-vision | | `provider` | A reference to the [`Practitioner`](/docs/api/fhir/resources/practitioner) who is responsible for providing the service. | | Practitioner/dr-alice-smith | | `quantity` | The number of repetitions of the service that will be performed. | | 2 | | `unitPrice` | The price charged to the patient for a single unit of the service. This is the price that the provider charges for the service. | | $200 | | `facility` | A reference to the [`Location`](/docs/api/fhir/resources/location) or [`Organization`](/docs/api/fhir/resources/organization) where the service will be provided. | | Organization/example-hospital | | `detail` | A reference to the [`CarePlan`](/docs/api/fhir/resources/careplan) with details describing the service. | | CarePlan/improve-vision |
Example: A coverage eligibility request for a consultation {ExampleCode}
Example: A plan coverage and general benefits check {ExampleCode}
### Sending an Eligibility Check Request Once you have created your [`CoverageEligibilityRequest`](/docs/api/fhir/resources/coverageeligibilityrequest), you need to send it to the insurer. In addition to sending it directly to the insurer, there are services that simplify the process. Companies such as [Opkit](https://www.opkit.co/), [Availity](https://www.availity.com/), [Change Healthcare](https://www.changehealthcare.com/), [Waystar](https://www.waystar.com/) and [Candid Health](https://www.joincandidhealth.com/) allow you to send them eligibility checks directly. Unfortunately, these companies format their requests based on [X12 EDI Format](https://x12.org/examples/005010x279) rather than FHIR, so you will need to convert your [`CoverageEligibilityRequest`](/docs/api/fhir/resources/coverageeligibilityrequest) to the correct format. This is a good workflow to implement [Bots](/docs/bots/bot-basics) to convert your request, interface with the company's API, and send the request. Additionally, you can have a [Subscription](/docs/subscriptions) to listen for a response and have a bot handle that as well. ### Receiving a Response When you send your request, the insurer will review it and respond. This response will be modeled as a [`CoverageEligibilityResponse`](/docs/api/fhir/resources/coverageeligibilityresponse). | **Element** | **Description** | **Example** | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | | `outcome` | The outcome of the _processing_ of the request. Does NOT answer if the patient is eligible for coverage. | complete | | `disposition` | A human-readable description of the status of the request. | The policy is currently in-force. | | `error` | Documents any errors that encountered during the eligibility check. Describes why a check may not have been able to be completed. | Missing Identifier | | `insurance.item` | Details about the benefits, authorization requirements, and current benefits of the insurance. | [See below](#the-item-being-checked-for-coverage) | | `insurance.inforce` | A boolean indicating if the coverage is in force for the requested period. | true | | `insurance.benefitPeriod` | The term period of the benefits documented in the response. | 2023-01-01 – 2023-12-31 | | `insurance.coverage` | A reference to the patient's [`Coverage`](/docs/api/fhir/resources/coverage) resource. | Coverage/example-coverage | | `request` | A reference to the original [`CoverageEligibilityRequest`](/docs/api/fhir/resources/coverageeligibilityrequest) this is in response to. | CoverageEligibilityRequest/check-for-vision-coverage | | `purpose` | The reason the initial request was made. [See the `purpose` field in the above table](#creating-a-request) for the allowed valueset. | validation | | `status` | The status of the response. | active | | `insurer` | A reference to the [`Organization`](/docs/api/fhir/resources/organization) that is providing coverage and that sent the response. | Organization/blue-cross | | `patient` | A reference to the [`Patient`](/docs/api/fhir/resources/patient) the response is for. | Patient/homer-simpson | #### The Covered Items Like its request counterpart, the [`CoverageEligibilityResponse`](/docs/api/fhir/resources/coverageeligibilityresponse) also has an `item` element, however it is a property on the `insurance` element rather than directly on the resource (e.g. `CoverageEligibilityResponse.insurance.item` vs `CoverageEligibilityRequest.item`). The `item` property contains details about allowed products and services under the insurance policy. This field has some overlap with the request resource, but there are also significant differences between the two. | **Property** | **Description** | **Code System** | **Example** | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------- | | `benefit` | A description of the benefits allowed and used to date under the coverage. | | allowedMoney: $10000, usedMoney: $645.99 | | `description` | A more detailed description of the benefits or services that are covered. | | Vision is covered in this policy. | | `authorizationRequired` | A boolean indicating if authorization is required before providing service. | | true | | `authorizationSupporting` | Details about additional information or material needed to get authorization. | [`CoverageEligibilityResponse` Auth Support Codes](https://build.fhir.org/valueset-coverageeligibilityresponse-ex-auth-support.html) | Lab Report | | `excluded` | A boolean indicating if the service is excluded from the plan. | | false | | `network` | Indicates whether the benefits apply to in-network or out-of-network providers. | [Network Type Codes](https://build.fhir.org/valueset-benefit-network.html) | in | | `unit` | Indicates whether the benefits apply to an individual or to a family. | [Unit Type Codes](https://build.fhir.org/valueset-benefit-unit.html) | individual | | `term` | The term or duration during which service is covered. | [Benefit Term Codes](https://build.fhir.org/valueset-benefit-term.html) | annual | | `productOrService` | The product, drug, service, etc. that is being provided. | [CPT Codes](https://www.ama-assn.org/topics/cpt-codes) | 92340 - Fitting of eyeglasses | | `provider` | A reference to the [`Practitioner`](/docs/api/fhir/resources/practitioner) who is responsible for providing the service. | | Practitioner/dr-alice-smith | | `category` | The general type of the service or product being checked for eligibility. | | Vision Coverage | :::note[Coordination of Benefits] FHIR makes the `insurance` field on both the request and response an array, allowing for coordination of benefits across multiple insurance policies. When sending a request, if there are multiple insurances, the `CoverageEligibilityRequest.insurance.focal` field should be set to `true` on the specific coverage being checked. The `item` field is also an array on the `insurance` element of a [`CoverageEligibilityResponse`](/docs/api/fhir/resources/coverageeligibilityresponse) because it can represent multiple items that are covered under a specific insurance. When coordinating care among multiple policies, it can be common for multiple items from multiple coverages to be relevant to the check. :::
Example: A coverage eligibility response for a basic consultation {ExampleCode}
Example: A plan coverage and general benefits check response {ExampleCode}
### See Also - [Eligibility Check Sample](https://github.com/medplum/medplum/tree/main/examples/medplum-eligibility-demo) code from Medplum - [Eligibility/Benefits Check](https://youtu.be/K3q8DkdWs6I) explainer video on Youtube - [Candid Health Eligibility Check Guide](https://docs.joincandidhealth.com/api-reference/eligibility) - [Change Healthcare Bare Minimum Check](https://developers.changehealthcare.com/eligibilityandclaims/docs/use-bare-minimum-eligibility-requests) --- ## Representing Patient Insurance Coverage import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; import Example from '!!raw-loader!@site/../../examples/medplum-demo-bots/src/candid-health/send-to-candid.test.ts'; ### Introduction The [`Coverage`](/docs/api/fhir/resources/coverage) resource represents high-level insurance information for a patient, similar to what would be found on their insurance cards. This guide will go over the most relevant elements of [`Coverage`](/docs/api/fhir/resources/coverage) for digital health providers and is primarily focused on the U.S. system. The guide aligns with recommendations from two implementation guides - [US Core Guidelines](/docs/fhir-datastore/understanding-uscdi-dataclasses) - these are guidelines developed by the U.S. government and serve as the basis for all FHIR implementations in the U.S. - [CARIN Digital Insurance Card Guide](http://hl7.org/fhir/us/insurance-card/) - CARIN is a industry group, including members from Humana, Blue Cross, and Mitre, that expands upon the U.S. Core. ### Subscribers vs. Beneficiaries In FHIR's [`Coverage`](/docs/api/fhir/resources/coverage) resource, a distinction is made between plan subscribers and beneficiaries. The `Coverage.subscriber` element denotes the individual who has subscribed to the plan, often also known as the "policyholder". The `Coverage.beneficiary` element, on the other hand, refers to the person who is eligible to receive healthcare services under the plan. The `Coverage.relationship` element describes how the subscriber and beneficiary are related. The US Core guidelines recommend that this code should be selected from the [HL7 subscriber relationship valueset](http://hl7.org/fhir/R4/valueset-subscriber-relationship.html). :::note[Example] Consider an example where Mr. John Doe, an employee at a company, has an insurance policy that covers his family, including his daughter, Jane Doe. When representing Jane's insurance coverage, she would be listed as the `Coverage.beneficiary`, John would be listed as the `Coverage.subscriber`, and the `Coverage.relationship` would be set to `'child'`. ::: ### Insurance Member ID The US Core implementation of FHIR mandates every [`Coverage`](/docs/api/fhir/resources/coverage) to have an insurance member ID for the _subscriber_. This identifier can be included in one of two elements: `Coverage.subscriberId` or `Coverage.identifier`. If you decide to use `Coverage.identifier`, US Core requires that `Coverage.identifier.type` is drawn from the [HL7 identifierType code system](https://terminology.hl7.org/5.2.0/CodeSystem-v2-0203.html) with code `MB`. Check out the [example below](#example) for an example. ### Plan Types and Payors One of the most important fields in the Coverage resource is the plan type (`Coverage.type`). The United States offers many different types of insurance plans, including Health Maintenance Organizations (HMOs), Preferred Provider Organizations (PPOs), government employee programs, veteran's insurance, and many others. Each of these plan types has a distinct set of benefits and billing structures. The US Core guidelines strongly encourage the use of the [Source of Payment Typology (SOPT)](https://www.nahdo.org/sopt) code system for classifying US plan types. These SOPT codes are a hierarchical system of numeric codes that classify most US health plans. Another required field is `Coverage.payor` , which indicates the company or institution who will reimburse the provider for care. This should a reference to an [`Organization`](/docs/api/fhir/resources/organization) resource, to allow for search queries of all patients insured by the same payor. :::tip[Representing self-pay / cash-pay] Many digital providers have a significant patient population that pay out-of-pocket. It is a best practice to create [`Coverage`](/docs/api/fhir/resources/coverage) resources for these patients as well, to allow flexible handling of their [coverage stack](#primary-vs-secondary-coverage) as their insurance situation evolves. A self-pay patient's [`Coverage`](/docs/api/fhir/resources/coverage) should set `subscriber`, `beneficiary`, `relationship`, `type`, and `payor` as follows: ```ts { resourceType: 'Coverage', //... type: { text: 'Self-pay' coding: [ { system: 'https://nahdo.org/sopt', code: '81', display: 'Self-pay (Includes applicants for insurance and Medicaid applicants)', }, ], }, subscriber: { reference: 'Patient/example-patient', }, beneficiary: { reference: 'Patient/example-patient', }, relationship: { coding: [ { system: 'http://terminology.hl7.org/CodeSystem/subscriber-relationship', code: 'self', display: 'Self' }, ], }, payor: [ { reference: 'Patient/example-patient', }, ], //... }; ``` ::: ### Plan Classifications: Group, Plan Name, RXBIN, RXPCN Most insurance cards have a set of codes, known as "classifiers", that identify important billing information for the subscriber's plan. The `Coverage.class` is used to store these classification values in an array of (`type`, `value`, `name` ) tuples. FHIR recommends using the [coverage class valueset](https://hl7.org/fhir/R4/codesystem-coverage-class.html) to represent the class type. Below is a table of the coverage class codes in this valueset. | **Code** | **Definition** | | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | | [group](http://hl7.org/fhir/R4/codesystem-coverage-class.html#coverage-class-group) | An employee group | | [subgroup](http://hl7.org/fhir/R4/codesystem-coverage-class.html#coverage-class-subgroup) | A sub-group of an employee group | | [plan](http://hl7.org/fhir/R4/codesystem-coverage-class.html#coverage-class-plan) | A specific suite of benefits. | | [subplan](http://hl7.org/fhir/R4/codesystem-coverage-class.html#coverage-class-subplan) | A subset of a specific suite of benefits. | | [class](http://hl7.org/fhir/R4/codesystem-coverage-class.html#coverage-class-class) | A class of benefits. | | [subclass](http://hl7.org/fhir/R4/codesystem-coverage-class.html#coverage-class-subclass) | A subset of a class of benefits. | | [sequence](http://hl7.org/fhir/R4/codesystem-coverage-class.html#coverage-class-sequence) | A sequence number associated with a short-term continuance of the coverage. | | [rxbin](http://hl7.org/fhir/R4/codesystem-coverage-class.html#coverage-class-rxbin) | Pharmacy benefit manager's Business Identification Number. | | [rxpcn](http://hl7.org/fhir/R4/codesystem-coverage-class.html#coverage-class-rxpcn) | A Pharmacy Benefit Manager specified Processor Control Number. | | [rxid](http://hl7.org/fhir/R4/codesystem-coverage-class.html#coverage-class-rxid) | A Pharmacy Benefit Manager specified Member ID. | | [rxgroup](http://hl7.org/fhir/R4/codesystem-coverage-class.html#coverage-class-rxgroup) | A Pharmacy Benefit Manager specified Group number. | Refer to our [example below](#example) for a [`Coverage`](/docs/api/fhir/resources/coverage) resource that is tagged with multiple classifier codes. ### Patient Costs and Copays Healthcare in the U.S often involves cost-sharing measures with patients, commonly referred to as "copays" or "co-insurance". These measures represent the portion of healthcare costs that the patient is responsible for, separate from what the insurance covers. In FHIR, these cost-sharing measures can be represented using the `Coverage.costToBeneficiary` field, which is an array of such cost-sharing provisions. The [CARIN Digital Insurance Card guide](http://hl7.org/fhir/us/insurance-card/) (a superset of the US Core guidelines) recommends a set of standard codes for the `costToBeneficiary.type` field in the [C4DIC Extended Copay Type](http://hl7.org/fhir/us/insurance-card/CodeSystem-C4DICExtendedCopayTypeCS.html) value set. This value set provides a comprehensive list of codes to represent most common cost-sharing provisions in the U.S. :::tip[Representing Cost Sharing Measures as Text] In certain scenarios, cost-sharing provisions may not be easily representable as numeric or monetary values. The CARIN guide offers the [Beneficiary Cost as String](http://hl7.org/fhir/us/insurance-card/StructureDefinition-C4DIC-BeneficiaryCostString-extension.html) extension to represent these these provisions as text strings. It is then up to the client's billing engine to interpret this string when adjudicating claims for reimbursement. The `rx` provision in our [example below](#example) below demonstrates how to use this extension. ::: See our [detailed example below](#example) for a comprehensive example representation of these cost-sharing provisions. ### Primary vs. Secondary Coverage Some patients may have multiple forms of reimbursement, forming a "coverage stack" for any given procedure. For patients with multiple sources of insurance, FHIR uses the `Coverage.order` element to represent the order of use of the patient's coverages. The `order` attribute is an integer, with the primary insurance given the value of `1`, secondary insurance a value of `2`, and so forth. It's crucial to correctly assign these numbers as they indicate to the billing system the sequence in which the coverages are to be applied. :::tip[A note on Co-pay cards] Some manufacturers will offer [co-pay cards](https://www.goodrx.com/healthcare-access/drug-cost-and-savings/what-are-manufacturer-copay-cards) to reimburse patients for their out-of-pocket costs for certain drugs. These cards can be modeled as additional [`Coverage`](/docs/api/fhir/resources/coverage) resources, with higher / lower priority on the coverage stack, as appropriate. To date, the community has not yet aligned on a standard for modeling patient reimbursements (rather than costs). Medplum is working with the FHIR community to define a standard, and currently recommends using a negative value for `Coverage.costToBeneficiary.value[x]`. ::: ### Storing Insurance Card Photos Digital health implementations often need to store photos of a patient's insurance card, alongside their coverage information. While FHIR does not support the direct storing of images within the Coverage resource, you can use the `DocumentReference` resource to store the image and connect it to the [`Coverage`](/docs/api/fhir/resources/coverage) using the `DocumentReference.context.related` element. See [External files and Document references](/docs/fhir-datastore/external-documents) for more information. ### A Detailed Example Below is a detailed example of a [`Coverage`](/docs/api/fhir/resources/coverage) resource, tagged with the appropriate code systems. This example conforms to the US Core guidelines. This example has been tested as part of the the [Medplum Candid Demo Bot](https://github.com/medplum/medplum/tree/main/examples/medplum-demo-bots/src/candid-health), a [Medplum Bot](/docs/bots) that submits insurance information to [Candid Health](https://www.joincandidhealth.com/) for reimbursement. Check out the [full bot](https://github.com/medplum/medplum/blob/main/examples/medplum-demo-bots/src/candid-health/send-to-candid.ts) for an example of how to submit FHIR resources to the Candid API. {Example} ### See Also - [US Core Guidelines](/docs/fhir-datastore/understanding-uscdi-dataclasses) - [CARIN Digital Insurance Card Guide](http://hl7.org/fhir/us/insurance-card/) - [Medplum Candid Demo Bot](https://github.com/medplum/medplum/tree/main/examples/medplum-demo-bots/src/candid-health) --- ## Bot Basics Bots are an advanced Medplum feature that enable complex workflows. A **Medplum Bot** is a snippet of JavaScript code that can run on any resource change (create or update). This JavaScript code has access to a [**Medplum client**](../sdk) , which itself can invoke FHIR operations. **Medplum Bots** are run as [AWS Lambdas](https://aws.amazon.com/lambda/) and in heavily sandboxed environments. You can apply an [AccessPolicy](/docs/access/access-policies) to the Bot if you want to further reduce the data it can read and write. Bots are disabled by default for accounts. Contact info@medplum.com if you'd like to learn more. :::note[Bots in Local Development] If you want to run bots locally, you should use a VM Context. For more details see the [Running Bots Locally docs](/docs/bots/running-bots-locally). ::: ### Example uses Consider some of these Bot use cases: - Adding default values to blank or missing properties - Custom data validation for custom business rules - Creating communications for new patients - Creating notifications for new lab results or reports - Creating one or more resources for a Questionnaire Response These capabilities would normally require writing custom code, HTTP servers, webhooks, and managing credentials for a separate service. By using Bots, the entire logic is self contained and managed in one place. Like all FHIR resources in Medplum, the [Bot resource](https://app.medplum.com/Bot) is versioned with full history tracking, so you can see exactly what changed over time. ### Creating a Bot :::caution[Note] Bots are disabled by default. Contact your info@medplum.com if you'd like to learn more. ::: :::caution[Note] Bots are restricted to Project administrators. If you do not have access, contact your Project administrator. ::: To create a Bot, navigate to the [Project Admin panel](https://app.medplum.com/admin/project) and click "Create new Bot". ![Create a Bot](/img/app/bots/create_bot.png) On the next page you can enter a bot **name** and **description** (optional). You can also optionally set an [**access policy**](/docs/access/access-policies) on the Bot, which can restrict the read/write privileges of the bot's code. By default, Bots have read/write access to all resources. ![Enter Bot Properties](/img/app/bots/enter_bot_properties.png) Click "Create Bot" to save the Bot, and you will see an acknowledgement that the Bot has been created. ![Bot Created Acknowledgement](/img/app/bots/bot_created_acknowledgement.png) ### Editing a Bot You can see all Bots in your account on the Bot resource page: [https://app.medplum.com/Bot](https://app.medplum.com/Bot) Click on your new Bot and navigate to the **Editor** tab. This presents a code editing window where you can write your Javascript code. ![Bot Editor](/img/app/bots/BotResourcePage.gif) All Bots are simply a single TypeScript/JavaScript file with a standard async entry point function called `handler`. To start your bot, create a `handler` function as shown below in the **Editor** window. ```typescript import { BotEvent, MedplumClient } from '@medplum/core'; export async function handler(medplum: MedplumClient, event: BotEvent): Promise { // Your code here } ``` The following function arguments are available to the Bot code, to enable it to do the functionality it requires. | Name | Type | Description | | ------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | `medplum` | [MedplumClient](/docs/sdk/core.medplumclient) | An instance of the medplum JS SDK ([documentation](/docs/sdk)) | | `event` | [BotEvent](/docs/sdk/core.botevent) | The event object that triggered the Bot | | `event.bot` | `Reference` | Reference to the Bot resource that is executing | | `event.contentType` | `string` | The content type of the input (e.g., `application/fhir+json`, `text/plain`) | | `event.input` | `string` | `Resource` | `Hl7Message` | `Record` | The bot input, usually a FHIR resource or content that was posted to a bot endpoint | | `event.secrets` | `Record` | Map of project secrets accessible to the bot. See [Bot Secrets](/docs/bots/bot-secrets) for more details | | `event.traceId` | `string` (optional) | Trace ID for request correlation and debugging | | `event.requester` | `Reference` (optional) | Reference to the resource that requested the bot execution | | `event.headers` | `Record` (optional) | Headers from the original HTTP request, when invoked by HTTP request | In this example, we'll assume the input is a `Patient` resource and print out the patient's name. ```ts import { BotEvent, MedplumClient } from '@medplum/core'; import { Patient } from '@medplum/fhirtypes'; export async function handler(medplum: MedplumClient, event: BotEvent): Promise { const patient = event.input as Patient; const firstName = patient.name?.[0]?.given?.[0]; const lastName = patient.name?.[0]?.family; console.log(`Hello ${firstName} ${lastName}!`); return true; } ``` When you are done editing, click "Save" to save your Bot code to Medplum. #### Understanding the code Let's break this example down. ```ts import { Patient } from '@medplum/fhirtypes'; //.... const patient = event.input as Patient; ``` This first line casts the contents of event.input of type `Patient`. This allows the rest of the bot code to take advantage of TypeScript's strong type system, along with IDE autocomplete and ESLint verification. Medplum provides type definitions for all FHIR resources in the `@medplum/fhirtypes` package. ```ts const firstName = patient.name?.[0]?.given?.[0]; const lastName = patient.name?.[0]?.family; ``` This line extracts the first and last name for the patient. Since patients can have many different names (e.g. maiden name, common name, official name), `Patient.name` is an array. In this example, we will only consider the `0th` name entry. FHIR stores a person's first names, middle names, etc. in an array called `HumanName.given`, and the last name in the `HumanName.family` property. For more information, refer to the documentation for the [`HumanName` datatype](/docs/api/fhir/datatypes/humanname). Because these properties may be undefined, we make heavy use of the Javascript [optional chaining (`?.`) operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) to access these properties. ### Deploying a Bot Clicking "Save" in the **Editor** tab persists your Bot code to the Medplum database, but _doesn't_ deploy it to run in production. To deploy your bot, click the "Deploy" button. ![Deploy Button](/img/app/bots/deploy_button.png) This works well for initial prototyping, but as you get closer to a production implementation [deploying from the command line](/docs/bots/bots-in-production#deploying-your-bot) potentially as part of a CI/CD can be preferred. **Medplum Bots** are run as [AWS Lambdas](https://aws.amazon.com/lambda/) and in heavily sandboxed environments. You can apply an [AccessPolicy](/docs/access/access-policies) to the Bot if you want to further reduce the data it can read and write. #### Creating and Deploying Bot from your IDE instead of Editor tab Alternatively, you can also write the code for a Bot and deploy from within your IDE. - [Create a Bot](https://app.medplum.com/admin/project) on Medplum and note its `id`. (All Bots in your account can be found [here](https://app.medplum.com/Bot)) - Create a new typescript file (e.g. `my-bot.ts`) and copy the contents of `examples/hello-patient.ts` into your new file. - With the `id` of the Bot `id` in hand, add a section to `medplum.config.json` like so ```json { "name": "sample-account-setup", "id": "", "source": "src/examples/sample-account-setup.ts", "dist": "dist/sample-account-setup.js" } ``` Then, you can [deploy your bot from command line](/docs/bots/bots-in-production#deploying-your-bot) ### Executing a Bot Once your bot has been [saved](#editing-a-bot) and [deployed](#deploying-a-bot), it is time to execute the bot. There are a few different ways a bot can be executed: 1. Clicking the "Execute" button in the **Bot Code Editor** 1. Sending a POST request to the [`$execute` endpoint](/docs/api/fhir/operations/bot-execute) 1. Setting up a [Subscription](/docs/fhir-basics#listening-for-changes-subscriptions) to execute the Bot automatically based on changes (see next section). #### _Executing from the Code Editor_ The simplest way to execute a bot is to click the "Execute" button inside the Bot's **Editor** tab. This will execute the most recently deployed version of your Bot, with the `event.input` set to the contents of the **Input Pane**. ![Execute from Editor](/img/app/bots/execute_from_editor.png) In the output panel at the bottom right you will see the string "true", indicating that the execution was a success. To see the results of the execution, navigate to the **Event** tab. (You may need to reload the page.) The **Outcome Desc** column will show the result of the console.log. #### _Using the `$execute` endpoint_ You can also execute a bot programmatically by sending an HTTP `POST` request to the Bot's `$execute`. Below is an example request sent with [`cURL`](https://en.wikipedia.org/wiki/CURL): ```bash curl -X POST 'https://api.medplum.com/fhir/R4/Bot//$execute' \ --header 'Content-Type: ' \ --header 'Authorization: Bearer ' \ --data '' ``` Let's walk through each of the parameters here in more detail. ##### `BOT_ID` You can find the `id` of your Bot by clicking on the **Details** tab of the Bot resource. In this example, it is ` 43ac3060-ff20-49e8-9682-bf91ab3a5191` ![Find your Bot ID](/img/app/bots/find_bot_id.png) ##### `CONTENT_TYPE` | Content-Type | typeof `event.input` | Description | | ------------------------------------ | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `text/plain` | `string` | `` is parsed as plaintext string | | `application/json` | `Record` | `` is parsed as JSON-encoded object | | `application/x-www-form-urlencoded ` | `Record` | `` is parsed as URL-encoded string, resulting in a key/value map | | `application/fhir+json` | [`Resource`](/docs/api/fhir/resources) | `` is parsed as a [FHIR Resource](/docs/fhir-basics#storing-data-resources) encoded as JSON | | `x-application/hl7-v2+er7` | [`HL7Message`](../sdk/core.hl7message) | `` is a string that should be parsed as a pipe-delimited HL7v2 message. HL7v2 is a common text-based message protocol used in legacy healthcare systems | ##### `ACCESS_TOKEN` This is the `access_token` you receive after completing the OAuth authentication flow. See [this how-to](/docs/auth/client-credentials#connecting-to-the-service) for more information. ##### `INPUT_DATA` This is the input data that will be parsed according to `CONTENT_TYPE` and passed into your Bot as `event.input`. Read more in the [Bot `$execute`](/docs/api/fhir/operations/bot-execute) documentation. #### _Executing automatically using a [`Subscription`](/docs/fhir-basics#listening-for-changes-subscriptions)_ While using the `$execute` endpoint allows developers to trigger Bots from 3rd party applications, the most common way to execute a bot is to use a [FHIR subscription](/docs/fhir-basics#listening-for-changes-subscriptions) to trigger the Bot whenever a resource has been updated. Let's connect our bot to [`Patient`](/docs/api/fhir/resources/patient) resources. That means that the Bot code will run on any "create" or "update" operation to any [`Patient`](/docs/api/fhir/resources/patient). :::note[] Bots can be run as a cron job. [Click Here](/docs/bots/bot-cron-job) to learn more. ::: First, go to the [Subscription](https://app.medplum.com/Subscription) resources page. Then click on the "New..." ![New Subscription](/img/app/bots/new_subscription.png) To ensure the Subscription is running, change "Status" to `Active` ![Activate Subscription](/img/app/bots/subscription_active.png) Specify which Resources will trigger this subscription using a FHIR search string. If you're not familiar with FHIR search semantics, check out [this tutorial](../search/basic-search) for a primer. For this example, we will trigger the Subscription after a change to _any_ `Patient` resource. Change "Criteria" field to `Patient` ![Subscription Criteria](/img/app/bots/subscription_criteria.png) :::warning[Subscriptions on `AuditEvents`] The criteria of a subscription cannot be set to an [`AuditEvent`](/docs/api/fhir/resources/auditevent) resource. When a subscription is triggered it creates an [`AuditEvent`](/docs/api/fhir/resources/auditevent), so using it as criteria would create a notification spiral. ::: Next, we specify action should be taken when the subscription is triggered, using the "Channel" field. Because Bots are executed using HTTP requests, we will select the Channel "Type" as `Rest Hook` and the Channel "Endpoint" as as `Bot/`. ![Subscription Channel](/img/app/bots/subscription_channel.png) Change "Payload" to `application/fhir+json`. This is similar to the [CONTENT_TYPE](#content_type) field used by the `$execute` endpoint. ![Subscription Payload](/img/app/bots/subscription_payload.png) Finally, scroll down and click "OK". **Congratulations!** Now you have an active Subscription. ([View all Subscriptions](https://app.medplum.com/Subscription)) We can test our new subscription by creating a new `Patient`. First, go to the [Patient resources](https://app.medplum.com/Patient) using the top-left menu and clicking "Patient" ![Patient Sidebar](/img/app/bots/patient_sidebar.png) Next, click on the "New..." [button](https://app.medplum.com/Patient/new) ![New Patient Button](/img/app/bots/new_patient_button.png) Enter a sample name such as given "Jane" family "Doe". Then Scroll down and click "OK" ![Create new Patient](/img/app/bots/NewPatient.gif) Now, let's go back to our [`Subscription`](https://app.medplum.com/Subscription). On the Timeline, you should see an `AuditEvent` with the outcome of the JavaScript code execution. If everything worked as expected, you should see "Hello Jane Doe" logged as part of the `AuditEvent`. ![Audit Event](/img/app/bots/auditevent.png) If you want to see all `AuditEvents` sorted by most recent, you can use [this link](https://app.medplum.com/AuditEvent?_count=20&_fields=outcomeDesc,_lastUpdated,entity&_offset=0&_sort=-_lastUpdated). ![All Audit Event](/img/app/bots/all_audit_events.png) ### Software Development Lifecycle Bots written using the web editor are a great way to get started. If you would like to develop locally, test and deploy apps as part of your software development lifecycle, you refer to our next tutorial on [deploying Bots in production](./bots-in-production) ### Medplum Lambda Bots Size Limits Medplum Bots that use the AWS Lamda runtime are subject to AWS Lambda size constraints: - **Maximum compressed size**: 50 MB These limits apply to your bot code and any extra dependencies you include. The [bot layer](/docs/bots/bot-lambda-layer) already provides many common dependencies (like `@medplum/core`, `node-fetch`, etc.). --- ## Bot Code Organization and Build Setup As your integration grows, you will likely end up with multiple bots that share common logic — API client setup, FHIR resource transforms, shared constants, and so on. Without a strategy, this leads to duplicated logic across bot files that become hard to maintain, quickly growing to thousands of lines. This guide explains how to structure a multi-bot project so that shared code lives in one place, while each deployed bot remains a single self-contained file (as Medplum requires). :::note[Prerequisites] This guide assumes you're comfortable with the basics of writing and deploying bots. If not, start with [Bots in Production](/docs/bots/bots-in-production) first. ::: :::note[Coming from Python or C#?] This guide is TypeScript-only — Medplum's built-in bot runtime executes JavaScript. If you're writing TypeScript for the first time, the build step may feel unfamiliar. Think of it like a compiler output: you write modular source files, and the toolchain produces a single deployable artifact per bot. You never ship your `src/` folder — only the compiled output. ::: ### The Single-File Constraint Medplum deploys **one JavaScript file per bot**. This is by design: it keeps the deployment unit simple and portable across runtimes (Lambda, VM context). That file must contain your shared `src/` modules and any **third-party** libraries you depend on, **except** packages you mark as `external` because they are already supplied by the Medplum bot runtime (for AWS Lambda, the [bot Lambda layer](https://github.com/medplum/medplum/blob/main/packages/bot-layer/package.json)). For a single bot this is fine. For a project with many bots sharing common logic, you need a way to write modular source code while still producing a single file per bot at deploy time. The answer is a bundler. ### Project Structure Organize your source into two directories: ``` my-bots/ ├── src/ │ ├── bots/ # One file per bot │ │ ├── send-order.ts │ │ ├── receive-result.ts │ │ └── sync-patient.ts │ └── common/ # Shared utilities imported by bots │ ├── connection.ts │ ├── order-utils.ts │ └── constants.ts ├── dist/ │ └── bots/ # Compiled output — one file per bot │ ├── send-order.js │ ├── receive-result.js │ └── sync-patient.js ├── esbuild.mjs └── package.json ``` Each file in `src/bots/` exports a single `handler` function. Each file in `src/common/` is a plain TypeScript module — no bot-specific logic. ### Bundling with esbuild [esbuild](https://esbuild.github.io/) is a fast JavaScript bundler that compiles each bot into one deployable file, inlining your `../common/` imports and (by default) **npm packages** from `node_modules`. Packages listed in **`external`** are left as `require()` calls — on AWS Lambda, Medplum resolves those from the **bot Lambda layer**, so list every layer-provided package you import to avoid bloating the bundle or duplicating runtime libraries. The layer’s package catalog is defined in Medplum’s [`packages/bot-layer/package.json`](https://github.com/medplum/medplum/blob/main/packages/bot-layer/package.json). Other bundlers work too, as long as they support inlining local imports and marking those imports as external. #### Installation ```bash npm install --save-dev esbuild glob ``` #### esbuild.mjs ```js import esbuild from 'esbuild'; import { glob } from 'glob'; // Compile every .ts file in src/bots/ (excluding tests) const botEntryPoints = glob.sync('./src/bots/*.ts').filter((f) => !f.endsWith('.test.ts')); esbuild .build({ entryPoints: botEntryPoints, outdir: './dist/bots', bundle: true, // Inline local imports and npm deps not listed in external platform: 'node', format: 'cjs', loader: { '.ts': 'ts' }, target: 'es2020', external: [ // Every Medplum layer package you import should appear here (expand as needed). // Catalog: https://github.com/medplum/medplum/blob/main/packages/bot-layer/package.json '@medplum/core', '@medplum/definitions', ], footer: { js: 'Object.assign(exports, module.exports);' }, }) .then(() => console.log('Build complete')) .catch(() => process.exit(1)); ``` Add a build script to `package.json`: ```json { "scripts": { "build": "node esbuild.mjs" } } ``` Run with: ```bash npm run build ``` #### What the bundler does | Source | After bundling | |--------|---------------| | `import { connectToApi } from '../common/connection'` | Inlined into the bot's `.js` file | | `import { MedplumClient } from '@medplum/core'` | Left as `require('@medplum/core')` when listed in `external` — resolved at runtime from the layer | | `import { RRule } from 'rrule'` (after `npm install rrule`) | Inlined into the bot's `.js` file — not on the layer, so do **not** add `rrule` to `external` | Each file under `dist/bots/` contains every **non-external** dependency (your shared modules and bundled npm libraries). Your `src/common/` folder is never deployed separately — its code is merged into each bot that imports it. Layer packages stay as `require()` calls. #### Third-party npm dependencies To ship a one-off library (for example `rrule`, `lodash`, or a small HTTP helper): 1. **`npm install `** (dependency or devDependency according to your repo conventions). 2. **`import`** it from a bot under `src/bots/` or from `src/common/` as usual. 3. **Omit it from `external`** unless that exact package is already provided by the Medplum bot Lambda layer (see the `dependencies` in the same [`bot-layer` `package.json`](https://github.com/medplum/medplum/blob/main/packages/bot-layer/package.json) linked in the esbuild example). If it is not on the layer, esbuild **bundles** it into the compiled `.js` file. :::warning[Note] If you use **transpile-only** tooling or wrongly list a non-layer package in `external`, the output can still contain `require('some-package')`. The Medplum CLI uploads **only** that single `dist` file — there is no `node_modules` on Lambda — so the bot will **fail at runtime**. [Bots in Production](/docs/bots/bots-in-production) describes the deploy path, `medplum.config.json`, and related constraints (for example deployment size). ::: ### Writing Bots and Shared Modules #### Bot files (`src/bots/`) Keep bot files thin. They should only contain the `handler` function and high-level orchestration: ```ts // src/bots/send-order.ts import { BotEvent, MedplumClient } from '@medplum/core'; import { ServiceRequest } from '@medplum/fhirtypes'; import { connectToApi } from '../common/connection'; import { buildOrderPayload } from '../common/order-utils'; export async function handler(medplum: MedplumClient, event: BotEvent): Promise { const order = event.input as ServiceRequest; const client = await connectToApi(medplum); const payload = buildOrderPayload(order); await client.submitOrder(payload); } ``` #### Shared modules (`src/common/`) Shared modules are plain TypeScript — no Medplum-specific structure required: ```ts // src/common/connection.ts import { MedplumClient } from '@medplum/core'; export async function connectToApi(medplum: MedplumClient): Promise { const secret = await medplum.getProjectSecret('API_KEY'); return new ApiClient(secret); } ``` #### What belongs in `common/` Move code to `src/common/` when: - Two or more bots use the same function - The logic is independently testable (API clients, data transforms, validators) - It's a constant or configuration value shared across bots Keep code in the bot file when: - It's only used by that bot - It's thin glue logic specific to one workflow ### Deployment The deploy workflow is the same as described in [Bots in Production](/docs/bots/bots-in-production). Your `medplum.config.json` points each bot at its source and compiled files: ```json { "bots": [ { "name": "send-order", "id": "", "source": "src/bots/send-order.ts", "dist": "dist/bots/send-order.js" }, { "name": "receive-result", "id": "", "source": "src/bots/receive-result.ts", "dist": "dist/bots/receive-result.js" } ] } ``` Deploy all bots in one command: ```bash npm run build npx medplum bot deploy '*' ``` --- ## Cron Jobs for Bots You can add a cron job for your bot so it can automatically run from a schedule. This means you can set a repeatable time for the bot to automatically every minute, day, other month, etc. ### To add a scheduled timer for your Bot - Navigate to your [Bots Page](https://app.medplum.com/admin/bots) - Click on the Bot you would like create a Cron Job for - Click on the name of the Bot - When you click on the Bot's name and open up the page, you should see the url similar to this `https://app.medplum.com/Bot/` - Click on the Edit Tab ![Edit Tab](/img/tutorials/edit-bot-page.png) ### The Edit Form In the Edit Form, scroll down to the Cron and choose one of the two ways to create a cron job. One as a UI tool, or a direct Cron format `e.g * */3 * * *`. ![Edit Form Page](/img/tutorials/edit-form.png) Click Ok in the bottom of the page, and your bot will be added to the queue. ### Scheduling with the Cron resource A [`Cron`](/docs/api/fhir/medplum/cron) resource owns a schedule independently of the Bot, so the same Bot can run on several schedules, each under a different identity and with different input. Requires the `cron` project feature. | Element | Meaning | | ----------------- | ------------------------------------------------------------------------------------------------ | | `active` | Only an active job is scheduled. Setting it to false unregisters the job but keeps the schedule. | | `onBehalfOf` | The `ProjectMembership` whose identity and access policy the run assumes. | | `targetReference` | The Bot to execute. | | `cronString` | The schedule, as a five-field cron expression. Required. | | `endTime` | The point after which the job stops running. | | `parameter` | Input for the Bot, which always receives the whole `Cron` resource, parameters included. | ```json { "resourceType": "Cron", "active": true, "cronString": "0 */3 * * *", "endTime": "2026-01-01T00:00:00.000Z", "onBehalfOf": { "reference": "ProjectMembership/" }, "targetReference": { "reference": "Bot/" }, "parameter": [{ "name": "region", "valueString": "us-east" }] } ``` Writing a `Cron` whose `cronString` is not a valid cron expression is rejected, so a broken schedule surfaces as an error on the request rather than as a job that silently never fires. The same applies to `targetReference` and `onBehalfOf`: a reference the author cannot read is rejected on write, rather than becoming a job that fails on every tick. Authoring a `Cron` is a project-administrator capability, since it chooses the identity the Bot runs as. #### Running a Bot from a linked project A Bot is code, not authority, so `targetReference` may name a Bot in a [linked project](/docs/access/projects#project-linking) while `onBehalfOf` may not. That is what lets a shared project publish a Bot once and each customer project schedule it: - The `Cron` and its `onBehalfOf` `ProjectMembership` live in the customer project, which needs both the `cron` and `bots` features. `onBehalfOf` can never name a membership in another project, linked or not — it chooses the access policy the run assumes, so allowing it would let one project borrow another's privileges. - The Bot lives in the shared project, which the customer project must link to. If the shared project sets `exportedResourceType`, it has to include `Bot`. The shared project does not itself need the `bots` feature to publish a Bot others run — that feature is checked against `onBehalfOf`'s project, since that is where the run executes — but it does need it to deploy one. - The customer project still needs a `ProjectMembership` of its own for that Bot to point `onBehalfOf` at. The Bot therefore runs with the customer project's access policy, reading and writing the customer's data, and its secrets are the shared project's overlaid with the customer's. The execution `AuditEvent` and the recorded Bot input both belong to the customer project — that is the identity the run assumed, and the input is the customer's data — so the publishing project sees its Bots' outcomes through server logs and bot execution metrics instead. A job that can no longer run stops itself on its next tick rather than failing indefinitely. Revoking the link, deleting the target Bot, and deleting the `onBehalfOf` `ProjectMembership` each unregister it. Removing the `cron` feature only pauses it, so restoring the feature resumes the existing schedule. --- ## Bot for QuestionnaireResponse Bots are an advanced Medplum feature that enable complex workflows. One of the most powerful uses is to combine Bots with [Questionnaires](/docs/api/fhir/resources/questionnaire) A FHIR [Questionnaire](/docs/api/fhir/resources/questionnaire) is a customizable form. You can add custom questions, question types, multiple choice options, etc. You can think of a FHIR Questionnaire as a healthcare-specific Google Forms or Survey Monkey. Connecting a Bot to a Questionnaire enables custom workflows that you control top to bottom. ### Example uses Consider some of these Bot and Questionnaire use cases: - Patient registration - Create a Patient plus initial [Appointment](/docs/api/fhir/resources/appointment) or [ServiceRequest](/docs/api/fhir/resources/servicerequest) - Observation entry - From one form, create many [Observation](/docs/api/fhir/resources/observation) and [DiagnosticReport](/docs/api/fhir/resources/diagnosticreport) resources - Quick ordering - Create shortcut forms for common orders or workflows These capabilities would normally require writing custom code, HTTP servers, webhooks, and managing credentials for a separate service. By using Bots, the entire logic is self contained and managed in one place. Like all FHIR resources in Medplum, the Bot resource is versioned with full history tracking, so you can see exactly what changed over time. ### Patient registration example Let's create a simple example Patient Registration form to see how this works. We'll write a simple questionnaire to collect information from a patient, and then write a Bot that creates the corresponding [Patient](/docs/api/fhir/resources/patient) and [ServiceRequest](/docs/api/fhir/resources/servicerequest) resources. #### 1. Create the Questionnaire import questionnaireSidebarURL from './questionnaire-sidebar.png' Our first step will be to create the questionnaire in the Medplum App 1. Log into the [Medplum App](https://app.medplum.com) 2. Click "Questionnaire" on the left sidebar or navigate to app.medplum.com/Questionnaire 3. Click "new" 4. This brings you to the default Resource editor. However, we are going to use an alternate method to author the Questionnaire. For now: 1. Enter a "Title" for your new Questionnaire 2. Scroll to the bottom of the page and click "OK" 5. Once you click "OK", navigate to the "Builder" Tab to actually create the Questionnaire #### 2. Edit the Questionnaire On the "Builder tab", you can add items to the Questionnaire with a Google Forms-like interface. ![Builder Tab](./builder-tab.png) Click the "Add Item" link at the bottom of the page to add each question. For each question, you will have to set the following properties: - **Type**: Select the question type from the dropdown - **Text:** The displayed text for the question - **Link Id**: The machine-readable identifier for the question. While Medplum supplies a default `linkId`, **_we highly recommend that users supply their own ids_** that are semantically relevant in their particular context. For this sample patient registration form, we will create five questions: - First Name (type: `string`, linkId: `firstName`) - Last Name (type: `string`, linkId: `lastName`) - Email (type: `string`, linkId: `email`) - Phone (type: `string`, linkId: `phone`) - Reason for Visiting (type: `choice`, linkId: `reasonForVisit`) ![Completed Questionnaire](./completed-questionnaire.png) #### 3. Submit the Form When you’re done, click on the “Preview” tab to see how your questionnaire will look. At the top, you will see this notice: ![Form Link](./form-link.png) To actually fill out the form, you will need to click on this link to be taken to the "form" page for your Questionnaire. You can also share this link with other users. Click on your form, fill it out, and click “Submit”. Now we’ll inspect the data. Click on the “Review your Answers” link. import reviewAnswersUrl from './review-answers.png'; Let’s take a look at the resource that is created: - The resulting resource is of type [QuestionnaireResponse](/docs/api/fhir/resources/questionnaireresponse) - The main property of interest is called `item`, all the form questions as well as their answers. - Each item has a `linkId` that you set in the builder, as well an answer array. Most of the time this array will contain 0 or 1 elements, but it could contain multiple elements in the case of multi-choice fields - The `questionnaire` property contains a reference to the original questionnaire that generated this response #### 4. Write the Bot Next, we'll write a Bot that creates a [Patient](/docs/api/fhir/resources/patient) and [ServiceRequest](/docs/api/fhir/resources/servicerequest) based on the user's response. To learn how to set up a new Bot, see the [Bot Basics tutorial](./bot-basics) To parse out the answers in the '[QuestionnaireResponse](/docs/api/fhir/resources/questionnaireresponse), we'll use the [`getQuestionnaireAnswers`](/docs/sdk/core.getquestionnaireanswers) utility function. This function returns a map from the question's `linkId` to the response. ```ts // Use the getQuestionnaireAnswers utility function to convert the response into // a map of [linkId, answer] pairs. const response = event.input as QuestionnaireResponse; const answers = getQuestionnaireAnswers(response); // Read out the user's answers into separate variables // Here we provide default answers if the user's answer is 'undefined' const firstName = answers['firstName']?.valueString || ''; const lastName = answers['lastName']?.valueString || ''; const email = answers['email']?.valueString || 'No Email Given'; const phone = answers['phone']?.valueString || 'No Phone Number Given'; const reasonForVisiting = answers['reasonForVisit']?.valueString || 'No Reason Given'; ``` We can combine this with the [`medplum.createResource`](/docs/sdk/core.medplumclient.createresource) method to create the [Patient](/docs/api/fhir/resources/patient) and [ServiceRequest](/docs/api/fhir/resources/servicerequest) . The final Bot then looks like this: ```ts import { BotEvent, MedplumClient, getQuestionnaireAnswers, createReference } from '@medplum/core'; import { QuestionnaireResponse, Patient, ServiceRequest } from '@medplum/fhirtypes'; export async function handler(medplum: MedplumClient, event: BotEvent): Promise { const response = event.input as QuestionnaireResponse; // Use the getQuestionnaireAnswers utility function to convert the response into // a map of [linkId, answer] pairs. const answers = getQuestionnaireAnswers(response); // Read out the user's answers into separate variables // Here we provide default answers if the user's answer is 'undefined' const firstName = answers['firstName']?.valueString || ''; const lastName = answers['lastName']?.valueString || ''; const email = answers['email']?.valueString || 'No Email Given'; const phone = answers['phone']?.valueString || 'No Phone Number Given'; const reasonForVisiting = answers['reasonForVisit']?.valueString || 'No Reason Given'; // Create the patient const patient = await medplum.createResource({ resourceType: 'Patient', name: [ { given: [firstName], family: lastName, }, ], telecom: [ { system: 'email', value: email, }, { system: 'phone', value: phone, }, ], }); console.log('Created Patient ', patient.id); // Create the Service Request const serviceRequest = await medplum.createResource({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: createReference(patient), reasonCode: [ { text: reasonForVisiting, }, ], }); console.log('Created ServiceRequest ', serviceRequest.id); } ``` #### 5. Create the Subscription The last step is to have your Bot listen to your Questionnaire responses. 1. Go to you the your Questionnaire's page in the [Medplum App](https://app.medplum.com/Questionnaire) 2. Click on the the "Bots" tab 3. Start typing the name of your Bot. 4. When your Bot appears in the type ahead, select it and click "Connect" import connectBotUrl from './connect-bot.png'; :::caution[Ordering of Subscription Events] Because of the variable timing and routes of http traffic, the Medplum server cannot guarantee the order in which [`Subscription`](/docs/api/fhir/resources/subscription) webhooks are received by the client. ::: #### 6. Watch it work Now if you [submit the form](#3-submit-the-form) again, you'll notice that a new [Patient](/docs/api/fhir/resources/patient) and [ServiceRequest](/docs/api/fhir/resources/servicerequest) have been created. ![Submit Form](./submit-form2.png) ![Review Patient](./review-patient.png)![Review Service Request](./review-service-request.png) ### Conclusion The Bots + Questionnaire patten is a powerful way to use Medplum to automate your medical workflows. While this was a very simple example, you can check out the [Medplum Demo Bots repo](https://github.com/medplum/medplum-demo-bots/) for more examples on how to use Bots. --- ## Medplum Bot Layers ### Overview of Medplum Bots Medplum Bots are functions that execute upon specific triggers. Think of them as AWS Lambda functions optimized for the Medplum environment. They are written, deployed, and triggered via FHIR Subscriptions, streamlining various integrations in Medplum. ### Introduction to AWS Lambda Layers Lambda Layers are .zip file archives that contain additional code or data, primarily used for library dependencies, custom runtimes, or configuration files. The reasons to use Lambda Layers are practical: 1. **Size Management**: By moving function dependencies to a layer, deployment packages remain smaller. 2. **Code Organization**: Layers allow for separation between core function logic and its dependencies. This means either can be updated independently. 3. **Dependency Sharing**: Layers make it possible to set up dependencies once and use them across multiple functions. 4. **Utilizing Lambda Console Code Editor**: For quick code edits, the editor is useful but has limitations with larger deployment packages. Layers can help mitigate this size issue. ### How Medplum Uses Lambda Layers Medplum's process for Lambda Layers is straightforward: - **Layer Creation**: We take the predefined dependencies' pinned version, organize them into the required folder structure, and then upload to AWS. - **Publication**: Bot Layers don't follow the typical Medplum update cycle, which is post every Git commit. Instead, we update Bot Layers with our release versions, approximately once a week. This aligns with the update frequency of some dependencies in the NPM central repository. ### A Note for Self-Hosters If you're hosting Medplum on your own infrastructure, remember that Bot layers don't auto-publish. To update the Bot Layer: 1. [Clone the Medplum Repo](/docs/contributing/local-dev-setup) (**Note:** You can clone the main Medplum repo instead of creating a fork) 2. [Build the Project](/docs/contributing/run-the-stack) 3. Deploy: Utilize `./scripts/deploy-bot-layer.sh` to deploy the new version. ### Learn More - [Working with Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html) - [Creating and sharing Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html) --- ## Run as User By default, the AccessPolicy that is applied to a Medplum Bot is defined by the Bot's [ProjectMembership](/docs/api/fhir/medplum/projectmembership). This can be useful for many cases where the Bot should have different access to resources than the user who triggered the Bot. However, there are cases where you may want the __Bot to inherit the access of the user who triggered it__. To do this, you can set the `runAsUser` field on the [Bot](/docs/api/fhir/medplum/bot) to true. Example: ```typescript const Bot = await medplum.createResource({ resourceType: 'Bot', name: 'My Bot', runAsUser: true, }); ``` Then, when you execute the Bot, it will inherit the access of the User who triggered it instead of the Bot. For example, if [client credentials auth](/docs/auth/client-credentials) are used to generate the access token that is used to execute the Bot, then the Bot's access will be defined by the AccessPolicy on the ProjectMembership for that Client Application that was used to trigger the Bot. --- ## Bot Secrets ### Introduction One of the use cases for Bots is to connect to 3rd-party APIs, which often involves using sensitive data, such as API keys. To avoid hard-coding these keys, Medplum provides the ability to store secrets in your Project. The advantage of handling secrets this way is: - It avoids leaking keys to unauthorized users - You can use different keys for in different projects. For example, you might have a "development" project to prototype your integration, and a "production" that serves your customers. ### Saving Secrets Secrets are stored on a per-project basis, and you must be a project administrator to edit secrets. 1. Go to the **Project Admin** page (either by clicking "Project" on the left sidebar, or navigating to https://app.medplum.com/admin/project) 2. Click on the "Secrets" tab 3. Click "Add" to add your new secret. Secrets can have type `string`, `number`, or `boolean`. ![Secrets Tab](/img/tutorials/bot-secrets/secrets-tab.png) ### Using Secrets in Your Bot Secrets can be accessed from the `event.secrets` property, which contains an map from the secret name to the `ProjectSecret` object. [See here](/docs/sdk/core.botevent.secrets) for more details about `event.secrets` ```ts import { BotEvent, MedplumClient } from '@medplum/core'; export async function handler(medplum: MedplumClient, event: BotEvent): Promise { // Print one secret console.log(`Secret: ${event.secrets['MY_API_KEY'].valueString}`); return true; } // Output: 'Secret: 123456' ``` --- ## Bots in Production Editing bots in the web editor is good for getting started quickly, but as Bots become more important you will want to manage them as part of your regular software development lifecycle. This means: - Storing bot code in source control (typically git) - Writing unit tests for your bots - Deploying your bots as part of your CI/CD pipeline. :::note[Bots in Local Development] If you want to run bots locally, you should use a VM Context. For more details see the [Running Bots Locally docs](/docs/bots/running-bots-locally). ::: ### This Guide will show you - How to set up a repository to host the source code for your Bots. - Write a new `Bot` in TypeScript. - Create a new `Bot` resource and link it to your TypeScript file. - Use the [Medplum Command Line Interface (CLI)](https://github.com/medplum/medplum/tree/main/packages/cli) to create and deploy your Bot to production. ### Setting up your Repository The first thing we'll do is set up a Git repository to host your Bot code. While you can set up bots in any git repository, we provide a [template Git repository](https://github.com/medplum/medplum-demo-bots) to help you get started. The Medplum Bot SDK requires [Node.js](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Version 22+ is required. Clone the repo and install the dependencies ```bash git clone git@github.com:medplum/medplum-demo-bots.git my-bots cd my-bots npm install ``` ### Setting up your Permissions Because Bots contain important or sensitive code, it's important to prevent unauthorized users from modifying your Bots. Medplum uses the [client credentials workflow](/docs/auth/client-credentials) authenticate the [Medplum CLI](https://github.com/medplum/medplum/tree/main/packages/cli). First, you should create a Client Application on the Medplum Server by following [these directions](/docs/auth/client-credentials). The [Medplum CLI](https://github.com/medplum/medplum/tree/main/packages/cli) looks for two environment variables when authenticating: `MEDPLUM_CLIENT_ID` and `MEDPLUM_CLIENT_SECRET`. You can set these on the command line using the `export` command in bash. ```bash export MEDPLUM_CLIENT_ID= export MEDPLUM_CLIENT_SECRET= ``` Alternatively, you can create a `.env` file to avoid having to re-export the environment variables in every new terminal. The example repository has a `.env.example` file you can copy to get started. ```bash cp .env.example .env ``` ```bash # .env MEDPLUM_CLIENT_ID= MEDPLUM_CLIENT_SECRET= ``` :::danger[Warning] **Your `.env` file should _never_ be checked into source control.** `MEDPLUM_CLIENT_ID` and `MEDPLUM_CLIENT_SECRET` should be considered sensitive security credentials and should never be shared in a publicly accessible store. The `medplum-demo-bots` repository adds `.env` to `.gitignore` by default. ::: If you are self-hosting Medplum, set `MEDPLUM_BASE_URL` to the base URL of your Medplum server as an environment variable or in your .env file. ```bash export MEDPLUM_BASE_URL=https://api.example.com/ ``` ```bash # .env MEDPLUM_BASE_URL=https://api.example.com/ ``` ### Create a source file After we've installed dependencies, we can write your Bot in any typescript file under the `src/` directory. As mentioned in [Bot Basics](./bot-basics), a bot is any TypeScript file that contains a `handler` function with the following signature: ```ts import { MedplumClient, BotEvent } from '@medplum/core'; export async function handler(medplum: MedplumClient, event: BotEvent): Promise { // Your code here } ``` See the [Bot Basics tutorial](./bot-basics#editing-a-bot) for more details about the arguments to `handler`. The starter repository contains an example Bot in the `examples` directory called `hello-patient.ts`. You can copy this example file to get started on your own bots: ```bash cd src cp examples/hello-patient.ts my-first-bot.ts ``` You'll see that this creates a simple bot that logs the patient's name to the console. For more details on how this code works, check out the [Bot Basics](./bot-basics/#editing-a-bot) tutorial. ```ts // src/my-first-bot.ts import { BotEvent, MedplumClient } from '@medplum/core'; import { Patient } from '@medplum/fhirtypes'; export async function handler(medplum: MedplumClient, event: BotEvent): Promise { const patient = event.input as Patient; const firstName = patient.name?.[0]?.given?.[0]; const lastName = patient.name?.[0]?.family; console.log(`Hello ${firstName} ${lastName}!`); return true; } ``` ### Compiling your Bot Congratulations! You’ve just written your first bot. Our next step will be to compile this code and link it to a [`Bot` resource](/docs/api/fhir/medplum/bot). First, compile your code: ```bash npm run build ``` This runs the `tsc` compiler to translate your TypeScript code to Javascript. Next, take a look at your `dist/` directory and notice how there is now a file called `my-first-bot.js` with the compiled version of your code. ```bash cd .. ls dist # my-first-bot.d.ts # my-first-bot.js # my-first-bot.js.map # examples/ # ... ``` ### Creating your Bot Next step is to create the bot. Navigate to the [Project Admin panel](https://app.medplum.com/admin/project) and copy the ID of your project. That will be your `project-id`. Taking the `source-file` we just created at `src/my-first-bot.ts`, we will use the `bot create` command. In our example ```bash npx medplum bot create ``` Running this command does the following: 1. Creates the Bot resource 2. Creates a ProjectMembership resource that connects it to a project 3. Saves the bot to the associated project in the Medplum database 4. Adds a bot entry to the `medplum.config.json` file in the `bots` array :::caution[Note] If you see an error, try running the command again. If it fails after 3 tries, please [**submit a bug report**](https://github.com/medplum/medplum/issues/new) or [**contact us on Discord**](https://discord.gg/medplum) ::: After creating the bot, you should go to `medplum.config.json` and you should see the new bot added in the bottom of the file. It should look like this: ```js { "bots": [ //… { "name": "my-first-bot", "id": "", "source": "src/my-first-bot.ts", "dist": "dist/my-first-bot.js" } ] } ``` | Parameter | Description | | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Name of the bot used in the [Medplum CLI](https://github.com/medplum/medplum/tree/main/packages/cli) (below). **Note**: This name can be whatever your want. It does not have to match the filename of the bot code, nor anything in the Medplum App | | `id` | The Bot Resource `id`. Can be found by navigating to [app.medplum.com/Bot](https://app.medplum.com/Bot) and clicking on the entry for the corresponding Bot. See the [Bot Basics tutorial](./bot-basics#bot_id) for more information | | `source` | This is the location of the typescript source file for your bot. **Note**: Currently, Medplum only supports single-file Bots. | | `dist` | This is the location of the transpiled javascript file for your bot. For most setups, this will be in your `dist` directory of your package. | ### Deploying your Bot Now that your Bot is written and compiled, the [resource](/docs/api/fhir/medplum/bot) is created, and your credentials are set, we can finally deploy our Bot to production using the [Medplum CLI](https://github.com/medplum/medplum/tree/main/packages/cli). To deploy our bot, we will use the `bot deploy` command. ```bash npx medplum bot deploy ``` Where `` is the `name` property that you set for your bot in `medplum.config.json`. In our example, this would be: ```bash npx medplum bot deploy my-first-bot ``` Use a wild card \* in `` to deploy multiple bots matching the pattern. This would allow us to deploy bots as part of a CI/CD pipeline, without having to update the command every time a new bot is added. ```bash npx medplum bot deploy *staging* ``` Running this command does two things: 1. Save the TypeScript source to the `code` property of your [`Bot` resource](/docs/api/fhir/medplum/bot) 2. Deploys your compiled Javascript code as an AWS Lambda function with your Medplum deployment. :::caution[Note] There is a known timing issue with the `bot deploy` command. If you see the following error, try running the command again. If it fails after 3 tries, please [**submit a bug report**](https://github.com/medplum/medplum/issues/new) or [**contact us on Discord**](https://discord.gg/medplum) ``` Deploy error: { resourceType: 'OperationOutcome', issue: [ { severity: 'error', code: 'invalid', details: [Object] } ] } ``` ::: ### Deploying to Staging vs. Production A common usage pattern is to set up two Medplum Projects for an application: A `staging` project for development and integration testing, and a `production` project to power workflows. The same Bot source code can be deployed to multiple Medplum projects by creating two separate entries in `medplum.config.json` with different names, pointing to the same source/compiled files. ```js { "bots": [ //… { "name": "my-first-bot-staging", "id": "", "source": "src/my-first-bot.ts", "dist": "dist/my-first-bot.js" }, { "name": "my-first-bot-production", "id": "", "source": "src/my-first-bot.ts", "dist": "dist/my-first-bot.js" } //... ] } ``` To deploy the latest Bot to staging: ```bash export MEDPLUM_CLIENT_ID= export MEDPLUM_CLIENT_SECRET= npm run build npx medplum bot deploy my-first-bot-staging ``` To deploy the latest Bot to production: ```bash export MEDPLUM_CLIENT_ID= export MEDPLUM_CLIENT_SECRET= npm run build npx medplum bot deploy my-first-bot-production ``` This pattern is especially powerful when deploying Bots **as part of a CI pipeline**. ### Configuring Bot Logging Bots can be run at a very high volume - for example as part of an ADT feed or when triggered by high-frequency messages. This can result in thousands of invocations per day, which can be overwhelming to track. In these cases, it may make sense to fine-tune the level of logging for your Bots. There are two ways to control Bot logging in Medplum: the type of event logged (i.e., success, failure, etc.) and where the event gets logged to. #### Logging Triggers You can choose to only log certain events using the `Bot.auditEventTrigger` field. This element represents the criteria for when an `AuditEvent` resource should be created and has four possible values. | Trigger | Description | | ----------- | ---------------------------------------------------------------------------------------------- | | `always` | An audit event is created and saved every time the bot runs. This is the default setting. | | `never` | An audit event will never be saved by the bot. | | `on-error` | An audit event is created and saved only if the bot throws an error. | | `on-output` | An audit event is created and saved whenever the bot logs any output as part of its execution. | #### Logging Destination Bot execution `AuditEvent` entries are **always** emitted to your server logs (e.g., [AWS CloudWatch](/docs/self-hosting/aws-athena-guide), [Datadog](/docs/self-hosting/datadog)). This cannot be disabled. The `Bot.auditEventDestination` field controls only whether the `AuditEvent` is **also written to the database**. | `auditEventDestination` | Logs | Database | | ----------------------- | ---- | -------- | | `resource` (default) | ✓ always | ✓ written | | `log` | ✓ always | ✗ not written | By default, `auditEventDestination` is `resource`, which writes the `AuditEvent` to the database in addition to logging it. This makes the event visible in the Medplum UI and queryable via the API — recommended for development and debugging. For high-volume bots, setting `auditEventDestination` to `log` prevents database writes while still preserving the full audit trail in your logging infrastructure. This avoids database growth and write overhead, at the cost of not being able to view events in the Medplum UI. ### Conclusion As your Bots become more complex, integrating them into your software development workflow becomes crucial. Using the [Medplum CLI](https://github.com/medplum/medplum/tree/main/packages/cli) allows you do integrate Bots into your regular code review process and deploy as part of your CI/CD pipelines. --- ## Consuming Webhooks Many SaaS products including popular services like Stripe and Okta support Webhooks, allowing a web application to register a Medplum URL to receive notifications. When a certain event occurs in the source application, such as a new user signup or a change to a record, the source application sends an HTTP POST request to the URL registered by the destination application. This HTTP POST request contains information about the event that occurred. If your source system supports HL7 CDS Hooks instead of vendor-specific webhooks, use [CDS Hooks](/docs/integration/cds-hooks) under Integrate Standards. Medplum [bots](/docs/bots) can be used to listen for webhooks and so keep records synchronized between systems. When another application fires a webhook, it can trigger a Medplum bot using the [$execute](/docs/bots/bot-basics#using-the-execute-endpoint) endpoint. For an example of a Bot that consumes webhooks, see the [Stripe demo bot](https://github.com/medplum/medplum-demo-bots/tree/main/src/stripe-bots). ### Planning your integration To get started, find the webhook documentation for the SaaS product you want to connect to Medplum. Find what type of data the webhook will send to Medplum and make note of it. For example, Stripe publishes an [event](https://stripe.com/docs/api/events/object) object specification that outlines the JSON that the webhook will post. ### Creating a Bot Once you understand the shape of the data you will consume, write a bot to parse the data and create or update the relevant FHIR resources. These bots are varied, but it can help to make a list of which [FHIR resources](/docs/api/fhir/resources) need to be manipulated when the Bot is triggered. The [demo bots repo](https://github.com/medplum/medplum-demo-bots) has examples across use cases We recommend writing some [unit tests](/docs/bots/unit-testing-bots) as well, and have several samples provided in `medplum-demo-bots` repo that use our `@medplum/mock` library for testing. #### Using TypeScript SDKs in your bot If the SaaS application that sends webhooks publishes a TypeScript SDK, it's straightforward to add it to your bot, to streamline development. Add the package to the `devDependencies` in the `package.json` of your bot repository and install the dependency, and [example from demo bots repo](https://github.com/medplum/medplum-demo-bots/blob/main/package.json) is available. You can then use the TypeScript SDK when developing your bot. The [Stripe demo bot](https://github.com/medplum/medplum-demo-bots/tree/main/src/stripe-bots) uses the Stripe TypeScript SDK. ### Creating Access Policies We recommend making at least one, but probably two Access Policies to enable Webhooks from applications. The reason these policies are important is that the allow the SaaS application to have very minimal access to the data in your Medplum project. Create an Access Policy that allows read only access to the Bot, like below. ```json { "resourceType": "AccessPolicy", "name": "Stripe Webhook Access Policy", "resource": [ { "resourceType": "Bot", "readonly": true } ] } ``` [Create a ClientApplication](/docs/auth/client-credentials) and assign the AccessPolicy above to that application and save. Create another (optional) one for the bot, that enables only the resources that bot touches and assign it to the bot in your [Admin settings](https://app.medplum.com/admin/project). ```json { "resourceType": "AccessPolicy", "name": "Stripe Bot Access Policy", "resource": [ { "resourceType": "Invoice" }, { "resourceType": "Account" } ] } ``` ### Configuring Webhooks in another Application The SaaS application that generates the webhooks will have a configuration for the endpoint url. In some cases they will support basic authentication in the URL itself. Construct one, like below using the client id, secret from the previous section and id of the bot you created. ```bash https://:@api.medplum.com/fhir/R4/Bot//$execute ``` ### Authenticating Payloads Many SaaS applications support signature verification. Their webhook configuration portal will have a place to download a `secret` or `signing secret`. You can store that value in [bot secrets](/docs/bots/bot-secrets) and use it to verify the webhook signature. ### Unauthenticated Webhooks Medplum supports unauthenticated webhooks for integrating with third-party services. This allows you to receive webhook notifications without requiring traditional authentication credentials in the URL. The webhook endpoint format is: ``` GET/POST /webhook/{ProjectMembership.id} ``` **Important Security Considerations:** Using unauthenticated webhooks inherently carries security risks. Medplum provides mechanisms to help you secure these endpoints, but **it is critical to implement strong security measures within your Bot and its configuration.** 1. **Bot `publicWebhook` Property (Required Opt-in):** Only Bots explicitly configured with `Bot.publicWebhook: true` can be executed via this endpoint. This prevents unintended exposure of Bots designed for internal use. 2. **Required Access Policy:** All Bots enabled for unauthenticated webhooks **must** have an associated `AccessPolicy`. This policy strictly defines the permissions and resources the Bot can access, minimizing its potential impact if compromised. 3. **Signature Verification (Crucial):** Always implement robust webhook signature verification within your Bot's code. Use the secret provided by the third-party service (e.g., Twilio's Auth Token) and store it securely in your Bot's secrets. Validate incoming requests against this signature to ensure they originate from the legitimate source. 4. **IP Whitelisting (Recommended):** If supported by the third-party service, configure IP whitelisting to only accept requests from known and trusted IP addresses. 5. **Rate Limiting (Recommended):** Implement rate limiting to prevent abuse or denial-of-service attacks against your webhook endpoint. Medplum offers platform-level rate limiting, but consider additional application-specific rate limiting within your Bot if appropriate. 6. **Payload Validation (Essential):** Always validate the structure and content of incoming webhook payloads before processing them. Do not trust external input. #### Verifying JSON signatures Bots receive parsed JSON in `event.input` by default. To verify a provider's signature against the original UTF-8 JSON text, set `Bot.rawBody` to `true`. With this setting enabled, `event.input` is the original text instead of a parsed object. Omitting the flag or setting it to `false` preserves the existing parsed-input behavior. Pass the raw `event.input` and the provider's signature header from `event.headers` to the provider's verification library. For Stripe, pass them with your webhook signing secret to `stripe.webhooks.constructEvent()`, which verifies the signature and returns the parsed Stripe event. Do not parse and reserialize the text first: doing so can change whitespace, escaping, and number formatting, invalidating the signature. Verify the signature before writing resources or processing the event. Parsed mode retains the existing JSON validation; raw mode still requires valid JSON but leaves further validation to your Bot or the provider's verification library. Both modes use the normal server JSON size limit. Body-parser decompresses compressed requests before capturing the text; use uncompressed UTF-8 JSON for providers that sign the transmitted bytes. The setting applies to public webhooks, the authenticated `$execute` operation, and custom operations implemented by the Bot; other content types and encodings retain their existing input handling, and instance-level custom operations still receive the target resource. Only one representation is sent to the Bot, so enabling raw input does not add a duplicate body to the invocation payload. Runtime payload limits still apply, including JSON escaping and invocation metadata. VM, standard and streaming AWS Lambda wrappers, and Fission already forward `input`; no wrapper redeployment is required solely for this feature. Public webhook routes do not provide a response stream. Existing Bots require no changes. Before enabling raw mode, update your handler to expect a string in `event.input` and to verify and parse that string using the provider's library. #### How to Set Up an Unauthenticated Webhook To set up an unauthenticated webhook, follow these steps: 1. **Create/Update your Bot:** - Create a new Bot or identify an existing Bot you wish to use. - **Crucially, set the `publicWebhook` property on your Bot resource to `true`**. - Ensure your Bot has an **appropriate `AccessPolicy`** linked. This policy should grant only the minimum necessary permissions for the Bot to perform its intended function. 2. **Implement Signature Verification in your Bot:** Your Bot's code should include logic to verify the incoming signature from the third-party service. This typically involves using a shared secret (stored as a Bot secret) and a hashing algorithm. 3. **Configure the Webhook URL in the Third-Party Service:** Use the Medplum webhook endpoint format (`https://api.medplum.com/webhook/{ProjectMembership.id}`) in your third-party service's webhook configuration. 4. **Test the Webhook Integration:** Use the third-party service's test tools to send a sample webhook notification and verify that your Medplum Bot executes correctly and processes the payload as expected. Check your Bot's logs for any errors related to signature verification or Access Policy. ### Monitoring your integration As data flows through the system, you can see event logs in your Medplum app as [AuditEvents](https://app.medplum.com/AuditEvent?_count=20&_fields=id,_lastUpdated,entity,type&_offset=0&_sort=-_lastUpdated). --- ## Create a PDF import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; import ExampleCode from '!!raw-loader!@site/..//examples/src/bots/creating-a-pdf.ts'; You can use Medplum Bots to create a PDF file as an attachment. There are always two steps: 1. Create the PDF as a FHIR `Binary` resource 2. Use the `Binary` resource as an attachment or content To create PDFs, Medplum Bots use [pdfmake](https://pdfmake.github.io/docs/0.1/). pdfmake uses a "Document Definition" model. You create a JSON object that defines paragraphs, styles, tables, etc. Medplum converts the document definition into a FHIR `Binary` resource with the PDF contents. To learn more about pdfmake, check out the [playground](http://bpampuch.github.io/pdfmake/playground.html) and [examples](https://github.com/bpampuch/pdfmake/tree/master/examples). ### FHIR Media This bot creates a PDF as a `Binary` and then creates a `Media` with the PDF as the content. {ExampleCode} ### Custom fonts Medplum has prebuilt support for the following fonts: - Helvetica - Roboto - Avenir You can set the default font in the pdfmake `defaultStyle`: ```ts const docDefinition = { content: { ... }, defaultStyle: { font: 'yourFontName' } } ``` You can also use inline styles: ```ts const docDefinition = { content: [ // If you don't need styles, you can use a simple string to define a paragraph 'This is a standard paragraph, using default style', // Using a { text: '...' } object lets you set styling properties { text: 'Hello world', font: 'yourFontName', }, ], }; ``` See the [pdfmake styling](https://pdfmake.github.io/docs/0.1/document-definition-object/styling/) page for more details. ### Embedded images You can load an image by URL and embed it in the PDF. {ExampleCode} See the [pdfmake images](https://pdfmake.github.io/docs/0.1/document-definition-object/images/) page for more details. --- ## Custom FHIR Operations Custom FHIR Operations is a powerful feature that allows you to extend the Medplum FHIR server with your own custom operations using the Medplum Bots framework. This feature enables you to create sophisticated business logic that integrates seamlessly with the FHIR API while leveraging the full power of JavaScript execution. ### What are Custom FHIR Operations? Custom FHIR Operations combine two key Medplum features: 1. **FHIR Operations** - Standard FHIR operations (like `$validate`, `$expand`, etc.) that can be invoked via HTTP requests 2. **Medplum Bots** - JavaScript functions that can be executed server-side with access to the Medplum SDK This integration allows you to: - Define custom business logic using JavaScript - Expose that logic as standard FHIR operations - Maintain full FHIR compliance while extending functionality - Process and transform FHIR resources programmatically - Integrate with external systems and APIs ### How It Works Custom operations are implemented through three main components: 1. **Bot** - Contains the JavaScript code that implements your custom logic 2. **OperationDefinition** - Defines the operation interface and links to your Bot 3. **Extension** - Links the `OperationDefinition` to the `Bot` implementation When a client calls your custom operation (e.g., `POST /fhir/R4/Patient/$my-custom-operation`), Medplum: 1. Looks up the corresponding `OperationDefinition` 2. Finds the linked Bot via the extension 3. Executes the `Bot` with the operation input 4. Returns the `Bot`'s output as the operation result ### How to Use Custom FHIR Operations #### Step 1: Create a Bot First, create a `Bot` that contains your custom logic: ```javascript exports.handler = async function (medplum, event) { const patient = event.input; // Your custom logic here patient.identifier = patient.identifier || []; patient.identifier.push({ system: 'https://example.com/patient-id', value: '12345', }); return patient; }; ``` See [Bot Basics](/docs/bots/bot-basics) for more details on creating Bots. **Important Requirements:** - Your Medplum project must have the `bots` feature enabled - The handler function receives `medplum` (SDK instance) and `event` (operation context) - `event.input` contains the operation input (request body for POST, query params for GET) - Return value becomes the operation output #### Step 2: Deploy the Bot Deploy your Bot code using the `$deploy` operation: ```bash curl -X POST "https://api.medplum.com/fhir/R4/Bot/{bot-id}/\$deploy" \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer {access-token}" \ -d '{ "code": "exports.handler = async function (medplum, event) { ... }" }' ``` #### Step 3: Create an OperationDefinition Create an `OperationDefinition` that describes your operation and links to your `Bot`: ```json { "resourceType": "OperationDefinition", "extension": [ { "url": "https://medplum.com/fhir/StructureDefinition/operationDefinition-implementation", "valueReference": { "reference": "Bot/{bot-id}" } } ], "name": "my-custom-operation", "status": "active", "kind": "operation", "code": "my-custom-operation", "system": true, "type": false, "instance": false, "parameter": [ { "use": "in", "name": "input", "type": "Patient", "min": 1, "max": "1" }, { "use": "out", "name": "return", "type": "Patient", "min": 1, "max": "1" } ] } ``` **Key Requirements:** - Must include the `operationDefinition-implementation` extension - Extension must reference a Bot resource - Define appropriate input/output parameters #### Step 4: Invoke Your Custom Operation Once deployed, you can invoke your custom operation like any standard FHIR operation: ```bash curl -X POST "https://api.medplum.com/fhir/R4/Patient/\$my-custom-operation" \ -H "Content-Type: application/fhir+json" \ -H "Authorization: Bearer {access-token}" \ -d '{ "resourceType": "Patient", "name": [ { "family": "Smith", "given": ["John"] } ] }' ``` ### Operation Types Custom operations support all standard FHIR operation types: - **System-level operations**: `/fhir/R4/$my-operation` - **Type-level operations**: `/fhir/R4/Patient/$my-operation` - **Instance-level operations**: `/fhir/R4/Patient/123/$my-operation` Configure the operation type using the `system`, `type`, and `instance` properties in your `OperationDefinition`. ### Input and Output Handling #### Input Processing - **POST requests**: Request body is passed as `event.input` - **GET requests**: Query parameters are passed as `event.input` - Access to full request context via `event.headers`, etc. #### Output Processing The `Bot`'s return value is automatically formatted according to the `OperationDefinition`'s output parameters. You can return: - FHIR resources directly - Primitive values (strings, numbers, booleans) - Complex objects that match the defined output structure ### Security and Permissions Custom operations inherit the security model of both Bots and FHIR operations: - Users must have permission to read the Bot resource - Bot execution follows the Bot's `runAsUser` configuration - Standard FHIR access controls apply to the operation endpoint - Operations can be system-level, type-level, or instance-level with appropriate permissions ### Error Handling If your Bot encounters an error: - Return an `OperationOutcome` resource for controlled error responses - Thrown exceptions will be caught and converted to appropriate HTTP error responses - Use standard FHIR error patterns for consistent client handling ### Example Use Cases - **Data validation**: Custom validation rules beyond standard FHIR constraints - **Data transformation**: Convert between different FHIR profiles or external formats - **Integration**: Connect to external systems, APIs, or databases - **Workflow automation**: Implement complex business processes - **Reporting**: Generate custom reports or analytics - **Decision support**: Implement clinical decision support algorithms --- ## Uploading Files import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; import ExampleCode from '!!raw-loader!@site/..//examples/src/bots/file-uploads.ts'; In digital health, a common requirement is to upload PDFs or other files from one system to another. Examples include: - Uploading pathology reports into a legacy EHR - Uploading service requisitions into an SFTP server In this guide, we will show you how to: - Create and upload a PDF file from a Bot to an HTTP endpoint - Upload a file form a to an SFTP server You can find complete example bots for these examples in the [Medplum Demo Bots repo](https://github.com/medplum/medplum-demo-bots) ### HTTP File Uploads To upload to a file using http, you will have to submit an HTTP request with the `Content-Type` `multipart/form-data`. Modern JavaScript runtimes provide the standard [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) API to make it easier to create form data. Let's take a look at an example. First, we'll create a pdf (see the [Create a PDF tutorial](./creating-a-pdf) for more details) {ExampleCode} [`medplum.createPdf()`](../sdk/core.medplumclient.createpdf) creates a [`Binary`](/docs/api/fhir/resources/binary) resource and stores it on the Medplum server. Our next step will be to download the resulting PDF data, and convert it to a stream to send to our 3rd party API. {ExampleCode} Next, we'll construct the request body using `FormData`. {ExampleCode} And lastly, we'll post the form to the API (here, we're just using [httpbin](#)) as an example {ExampleCode} This is what it looks like all put together. You can also see this example in our [Medplum Demo Bots](https://github.com/medplum/medplum-demo-bots/blob/main/src/form-data-upload.ts) repo. {ExampleCode} ### SFTP Uploads Medplum Bots provide the [ssh2-sftp-client](https://www.npmjs.com/package/ssh2-sftp-client) library to connect to SFTP servers. You can reference the library's [github page](https://github.com/theophilusx/ssh2-sftp-client) for detailed documentation on how it works. Below is an example bot that connects to an SFTP server and returns a list of all available files at the root directory. {ExampleCode} --- ## HL7 to FHIR import ExampleCode from '!!raw-loader!@site/..//examples/src/bots/hl7-into-fhir.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; HL7 interfaces are common in healthcare, and widely supported by legacy EHRs, RIS/PACS systems, lab machines and more. Medplum provides a [HL7 Interfacing engine](/docs/integration/hl7-interfacing) that supports consumption and production of HL7 feeds, and the [Medplum Agent](/docs/agent) supports connecting to systems on-premises. ### This guide will show you - How to integrate Medplum with an HL7 feed bi-directionally, receiving messages and sending back confirmation messages. - How to convert those HL7 messages into FHIR objects ### The Workflow When we this implementation is complete we will: - Receive an HL7 message over HTTPS from another EHR that produces HL7 feeds - Create FHIR objects that capture the parts of the feed we care about. - Respond to the EHR with an HL7 acknowledgment that the message was received - (Optional) Send a notification to another application, indicating that new FHIR resources are available. ### The Implementation #### Account and Policy Setup - Make sure you have an account on Medplum, if not, [register](https://app.medplum.com/register). - Create a [ClientApplication](https://app.medplum.com/admin/project) on Medplum called "ADT Bot Client Application". - (Optional) Create a very restrictive [AccessPolicy](https://app.medplum.com/AccessPolicy) called "ADT Bot Policy", make it so that the policy only allows readwrite on the Patient object. - (Optional) In the [ProjectAdmin dashboard](https://app.medplum.com/admin/project) apply the "ADT Bot Policy" policy to the `ClientApplication` by clicking `Access`. #### Bot Setup - Linking the ADT feed to Medplum is done through [Medplum Bots](https://app.medplum.com/Bot). - At a high level, each Bot exposes an endpoint and HL7 messages are posted over HTTPS to that endpoint. - The HL7 message is parsed and then converted to a corresponding FHIR object. - (Optional) If needed, you can link a [Subscription](https://app.medplum.com/Subscription) to the FHIR objects that the Bot creates to notify downstream applications that new data is available. - Make the bot that will listen for HL7 messages - First, [create a bot](https://app.medplum.com/admin/project) called ADT Handler Bot and save it - Paste the code below into the Bot you created and save. You can also find this bot and a corresponding test in the [Sample Bots Github Repository](https://github.com/medplum/medplum-demo-bots/). {ExampleCode} Functionally, the code above will create a new patient with the `mrnNumber` provided, assuming that patient isn't already in this system. #### Testing your Bot You'll need your bot id (see [bot list](https://app.medplum.com/Bot) and click) to execute the bot. Once you have found it, you can attempt to execute your Bot using an HTTP message by sending the following via curl. Note the content type. ```bash curl -X POST 'https://api.medplum.com/fhir/R4/Bot//$execute' \ --header 'Content-Type: x-application/hl7-v2+er7' \ --header 'Authorization: Bearer ' \ --data-raw 'MSH|^~\&|Primary||CL|PDMT|20200312081842|168866|ADT^A28|203598|T|2.3||||||||||| EVN|A28|20200312081842||REG_UPDATE|168866^GLOVER^JASMIN^^^^^^PHC^^^^^10010|| PID|1||E3866011^^^EPIC^MRN~900093259^^^EPI^MR||TESTING^UGA||20000312|M|||^^^^^USA^P||||||||123-54-8888|||||N||||||N|| PD1|||PHYSICIANS ATLANTIC STATION^^10010||||||||||||||| PV1|1|N|||||||||||||||||||||||||||||||||||||||||||||||||||| PV2||||||||||||||||||||||N|||||||||||||||||||||||||||' ``` If all goes well, you should see the following HL7 acknowledgement message in the console. ``` MSH|^~\\&|CL|PDMT|Primary||2022-05-10T16:19:50.244Z||ACK|1652199590244|P|2.5.1\rMSA|AA|203598|OK ``` Alternatively, you can submit the HL7 message type to the bot as a file using the following command. ```bash curl -x POST 'https://api.medplum.com/fhir/R4/Bot//$execute' \ --header 'Content-Type: x-application/hl7-v2+er7' \ --header 'Authorization: Bearer ' \ --data-binary "@/path/to/filename" ``` #### Creating a subscription If you want to receive a notification whenever a Patient (or other FHIR resource) is created, you can do so by creating a [Subscription](./bot-basics#executing-automatically-using-a-subscription). Subscriptions have a concept of `Criteria` which indicates when they should be triggered. Link them to the FHIR resource of choice. #### Complex Logic in Bots Creating a patient from a single type of HL7 message is straightforward and doesn't require much code. In practice, this is unrealistic, and the code will soon require a more complex application with testing and strongly typed objects. To support that we have a Bot toolkit for development and deployment that you can find here: [CLI Tool](https://github.com/medplum/medplum-demo-bots). --- ## Bots Medplum bots are functions that execute when triggered. They are similar to AWS Lambda functions, and in Medplum we make them easy to write and deploy and trigger with [FHIR Subscriptions](/docs/subscriptions). For the purpose of the tutorials found in this section, it's important to understand that **Bots drive many of the major integrations that you see in Medplum**. If you are new to Medplum Bots, we encourage you to read the intro material in the [**Bot Guide**](/docs/bots/bot-basics). The following tutorials will walk through some of the use cases for Bots, to give you a sense of how they can work for you. :::warning[Enabling Bots] Bots are a [project level setting](/docs/access/projects#settings) that must be enabled by a Super Admin user. For users on the [Medplum Hosted Service](/pricing), bots are a paid features. Contact info@medplum.com or reach out in our [Discord channel](https://discord.gg/medplum) to have Bots enabled on your project. Super administrators can enable bots via the Medplum App: - Navigate to https://app.your-medplum-domain.com/Project/:projectId - Click on the "Edit" tab - In the "Features" section, add an entry with the value "bots" ::: ### Getting Started Learn the fundamentals of building and testing bots before deploying them to production. - [Bot Basics](/docs/bots/bot-basics) - [Unit Testing Bots](/docs/bots/unit-testing-bots) - [Running Bots Locally](/docs/bots/running-bots-locally) ### Bot Triggers & Invocation Configure how and when your bots execute, from scheduled tasks to real-time event responses. - [Cron Jobs for Bots](/docs/bots/bot-cron-job) - [Consuming Webhooks](/docs/bots/consuming-webhooks) - [Bot for QuestionnaireResponse](/docs/bots/bot-for-questionnaire-response) - [Custom FHIR Operations](/docs/bots/custom-fhir-operations) ### Common Use Cases & Patterns Implement frequently-needed functionality like file handling, document generation, and data transformation. - [Uploading Files](/docs/bots/file-uploads) - [Create a PDF](/docs/bots/creating-a-pdf) - [HL7 to FHIR](/docs/bots/hl7-into-fhir) ### Deployment & Infrastructure Deploy your bots to Medplum's cloud infrastructure or integrate with your existing AWS environment. - [Bots in Production](/docs/bots/bots-in-production) - [Bot Code Organization and Build Setup](/docs/bots/bot-code-organization) - [Running Bots on Fission](/docs/bots/running-bots-on-fission) - [Medplum Bot Layers](/docs/bots/bot-lambda-layer) ### Security & Permissions Manage sensitive credentials and control what actions your bots can perform. - [Bot Secrets](/docs/bots/bot-secrets) - [Run as User](/docs/bots/bot-run-as-user) ### Monitoring & Operations Track bot performance and troubleshoot issues in production environments. - [Monitoring Bots](/docs/bots/monitoring-bots) ### Reference - [Demo Bots Repository](https://github.com/medplum/medplum-demo-bots) on Github - [Bot Features and Fixes](https://github.com/medplum/medplum/pulls?q=is%3Apr+label%3Abots) on Github --- ## Monitoring Bots Bots allow for powerful functionality in your app, so it is very important to track what exactly each bot has done. This is possible using a bot's `AuditEvent` resources, which track: - Each time the Bot runs - The outcome of the run - Any logs output by the run ### Viewing a Bot's Events To monitor your bots, navigate to the Bot resource page at https://app.medplum.com/Bot. This page will display all of the bots that are a part of your project. Choose which bot's events you would like to view and click on it or, if you know the bot's `id`, navigate directly to https://app.medplum.com/Bot/:id. This will bring you to the Bot's individual resource page. From this page, navigate to the `Event` tab, or https://app.medplum.com/Bot/:id/event. This tab will display all of the `AuditEvent` resources associated with the current bot. These events represent every time the bot has been triggered. This page displays the outcomes from the bot being run, including anything that is printed to the console as part of the bot's functionality. On the Event tab, there are four fields: ID, Outcome, Outcome Desc, and Last Updated. | Field | Description | | -------------- | ----------------------------------------------------------------------------------------------------------------------- | | `ID` | The id of the `AuditEvent` being displayed, _not_ the bot's id. | | `Outcome` | The result of the event (i.e. success, failure, etc.). There are four possible values. See the table below for details. | | `Outcome Desc` | A description of the results of the bot, including anything that was logged to the console by the bot. | | `Last Updated` | The last time the `AuditEvent` was updated. This is likely when the bot finished running. | The Outcome field represents the result of the event (i.e. success, failure, etc.), and is coded to four values. | Code | Display | Description | | ---- | --------------- | ------------------------------------------------------------------------------------------------------- | | `0` | Success | The bot ran successfully, though there may have been warnings. | | `4` | Minor failure | The bot was not able to complete due to a minor failure (often similar to an HTTP 400 response). | | `8` | Serious Failure | The bot was not able to complete due to a more serious failure (often similar to an HTTP 500 response). | | `12` | Major failure | The bot failed with a major error and the system is no longer available for use. | The Outcome Desc is a description of the results of the bot, including anything that was logged to the console by the bot. ### Troubleshooting #### Audit Events Missing It is possible that on the `Event` tab, there will not be any `AuditEvent` resources stored in the database. This is likely because the bot is configured to only write to the logs, which allows for large-scale bot execution. To ensure that your bot is writing to your app, you can configure your bot using the `Bot.auditEventDestination` field. Setting this field to `resource` will update it so that all events executed by your bot appear in your console. For more details see the [Configuring Bot Logging docs](/docs/bots/bots-in-production#configuring-bot-logging). --- ## Running Bots Locally To set up the Medplum [`Bot`](/docs/api/fhir/medplum/bot) framework locally, Medplum offers VM Context [`Bots`](/docs/api/fhir/medplum/bot). VM Context allows bots to spin up a local thread inside your server, rather than using an isolated lambda. Before enabling VM Context [Bots](/docs/api/fhir/medplum/bot), you must first enable bots on your project. To do so, follow thse steps: 1. Log in to your [Super Admin Project](/docs/self-hosting/super-admin-guide). 2. Access your [Project](/docs/api/fhir/medplum/project) resource. 3. Go to the `Edit` tab. 4. In the `Features` section, add the `bot` feature. :::note[] The `defaultProjectFeatures` server config setting is used for default features when your project is being set up. Editing this config setting will not update your project to enable bots. ::: Once this is done, you can enable VM Context bots. There are two steps to set up VM context bots: 1. Enable VM Context [`Bots`](/docs/api/fhir/medplum/bot) on your server config. 2. Set your [`Bot's`](/docs/api/fhir/medplum/bot) runtime version to VM Context. To enable VM Context [`Bots`](/docs/api/fhir/medplum/bot) on your server, set `vmContextBotsEnabled: true` in both the AWS parameter store and your local `config.json` file. All [`Bots`](/docs/api/fhir/medplum/bot) have a field for `runtimeVersion`, which can be set to either `awslambda` or `vmcontext`. To use your [`Bot`](/docs/api/fhir/medplum/bot) locally, set this field to `vmcontext`. Using VM Context allows you to use resources more efficiently, however it can also have security and isolation concerns if used in an untrusted environment. For this reason, it is important to only use VM Context [`Bots`](/docs/api/fhir/medplum/bot) in trusted environments. :::danger[] **The `node:vm` module is not a security mechanism. Do not use it to run untrusted code.** The code for these bots runs in the server, so they can potentially have access to sensitive information when run in production environments without appropriate safety measures. ::: --- ## Running Bots on Fission Medplum Bots provide powerful automation capabilities within your Medplum environment. While Medplum offers a built-in execution environment for bots, this page details how to deploy and manage your Medplum Bots using Fission, an open-source, Kubernetes-native serverless framework. This approach can be beneficial for organizations seeking advanced scalability, custom runtime environments, or deeper integration with existing Kubernetes infrastructure. ### What are Medplum Bots? Medplum bots are JavaScript code snippets that automate workflows within Medplum by reacting to changes in healthcare data (FHIR resources) and performing operations like data validation, notifications, or integrations. They are a core component of building dynamic and responsive healthcare applications on Medplum. ### What is Fission? Fission.io is an open-source, Kubernetes-native serverless framework that allows developers to deploy and run short-lived functions in any language, triggered by various events, without having to manage the underlying container infrastructure. It seamlessly integrates with Kubernetes, leveraging its orchestration capabilities for efficient function execution and scaling. This guide focuses on setting up the necessary infrastructure to run Medplum Bots as Fission functions. It covers the configuration, deployment, and management of these serverless functions, enabling them to respond to events originating from your Medplum instance. ### Fission Router Configuration To enable your Medplum application to communicate with your Fission functions, you must correctly configure the Fission Router's address. The `routerHost` and `routerPort` settings within your `FissionConfig` dictate how your application establishes a connection to the Fission Router. These settings specify the network address (hostname or IP address) and the port through which the Fission Router can be accessed. The optimal configuration depends on where your Medplum application is deployed relative to your Fission cluster. - **For applications running outside the Kubernetes cluster** (e.g., local development machine, external client): - `routerHost`: This should be the external IP address of a Kubernetes Node where Fission is running, or `localhost` / `127.0.0.1` if you are using a local Kubernetes setup like Docker Desktop. - `routerPort`: This must be the `NodePort` exposed by the Fission Router service. You can determine this by running `kubectl get svc -n fission router`. In many common Fission installations, this is often `31314`. - **For applications running inside the Kubernetes cluster** (e.g., another service within the same cluster): - `routerHost`: This should be the internal ClusterIP service name of the Fission Router. Kubernetes DNS will automatically resolve this name within the cluster. The standard format is `router.fission.svc.cluster.local`. - `routerPort`: This will typically be the standard HTTP port `80`, as the internal ClusterIP service usually exposes the router on its default HTTP port. ### Medplum Server Configuration for Fission Add your Fission router configuration to your Medplum server configuration. For example, when running a local development setup with Fission in a Docker Desktop Kubernetes cluster, add the following to `medplum.config.json`: ```json "fission": { "namespace": "default", "fieldManager": "medplum-fission-example", "environmentName": "nodejs", "routerHost": "localhost", "routerPort": 31314 } ``` When using AWS Parameter Store, you can add a single `fission` parameter with the following JSON value: ```json { "namespace": "default", "fieldManager": "medplum-fission-example", "environmentName": "nodejs", "routerHost": "localhost", "routerPort": 31314 } ``` ### Local Development Setup for Fission To run a pre-configured Medplum + Fission environment, you can follow the instructions in https://github.com/medplum/medplum/tree/main/examples/medplum-local-k8s/README.md Otherwise, to get started with Fission for local development or testing, follow these steps to install it on your Kubernetes cluster. For detailed and up-to-date installation instructions, always refer to the official Fission documentation: [https://fission.io/docs/installation/](https://fission.io/docs/installation/) ```bash # Define the namespace for Fission export FISSION_NAMESPACE="fission" # Create the Fission namespace kubectl create namespace $FISSION_NAMESPACE # Apply Fission's Custom Resource Definitions (CRDs) # The `ref` parameter pins the version for stability and reproducibility. # For production, always use a specific, tested version. # Check Fission's official documentation for the latest stable release. kubectl create -k "github.com/fission/fission/crds/v1?ref=v1.21.0" # Add the Fission Helm repository helm repo add fission-charts https://fission.github.io/fission-charts/ # Update your Helm repositories helm repo update # Install Fission using Helm # The --set serviceType=NodePort,routerServiceType=NodePort is crucial for external access in development helm install --version v1.21.0 --namespace $FISSION_NAMESPACE fission \ --set serviceType=NodePort,routerServiceType=NodePort \ fission-charts/fission-all ``` This setup will install Fission with NodePort services for its components, making it accessible from outside the cluster, which is ideal for local development and testing. ### Working with Dependencies in Fission Bots Medplum Bots often rely on external npm packages. When running your bots on Fission, Medplum handles the packaging and deployment of your bot code along with its `package.json` file. This allows Fission to install the necessary dependencies during the build process. Refer to the official Fission documentation for the most current information on managing dependencies: [https://fission.io/docs/usage/languages/nodejs/\#working-with-dependencies](https://fission.io/docs/usage/languages/nodejs/#working-with-dependencies) Here's a general workflow for preparing your bot: 1. **Create a Node.js Environment in Fission:** First, establish a Fission environment that includes the necessary runtime and builder images for Node.js. Medplum will use this environment to deploy and execute your Bot. ```bash fission environment create --name nodejs --image ghcr.io/fission/node-env --builder ghcr.io/fission/node-builder ``` 2. **Prepare your Medplum Bot Code:** Create your Medplum Bot's source file ```ts import { BotEvent, MedplumClient } from '@medplum/core'; import { Patient } from '@medplum/fhirtypes'; export async function handler(medplum: MedplumClient, event: BotEvent): Promise { const patient = event.input as Patient; const firstName = patient.name?.[0]?.given?.[0]; const lastName = patient.name?.[0]?.family; console.log(`Hello ${firstName} ${lastName}!`); return true; } ``` See [Bot Basics - Editing a Bot](/docs/bots/bot-basics#editing-a-bot) for more details on writing Medplum Bot code. 3. **Deploying your Bot with Medplum:** Unlike direct Fission deployment, **Medplum handles the packaging, Fission package creation, and Fission function creation automatically** when you deploy your bot. You simply define your bot code within Medplum, and Medplum's internal processes will manage the Fission-specific deployment steps. See [Bot Basics - Deploying a Bot](/docs/bots/bot-basics#deploying-a-bot) for more details on deploying your bot. 4. **Check Fission Build Status (for debugging):** After deploying your bot through Medplum, you can still monitor the Fission build status of the underlying package to ensure all dependencies are correctly installed and the function is ready. You'll need to know the name of the Fission package created by Medplum (which typically incorporates your bot's ID). ```bash # You might need to inspect Medplum logs or Fission resources to find the exact package name. fission package info --name ``` 5. **Test Your Fission Function (for debugging):** Once the package build is complete, you can test your Fission function directly. For Medplum Bots, you'd typically send a POST request with FHIR data to the bot's Fission endpoint. ```bash curl -X POST -H "Content-Type: application/json" \ -d '{ "resourceType": "Patient", "id": "123", "name": [{ "given": ["John"], "family": "Doe" }] }' \ "https://api.medplum.com/fhir/R4/Bot//\$execute" ``` See [Bot Basics - Executing a Bot](/docs/bots/bot-basics#executing-a-bot) for more details on executing your bot. ### Troubleshooting Fission Deployments When working with Fission, especially during initial setup or debugging, these commands can be invaluable: - **List Fission Functions:** See all deployed Fission functions in your cluster. ```bash kubectl get functions -n ``` - **Show a Fission Function Definition (JSON):** Get detailed configuration of a specific function. ```bash kubectl get function -o json -n ``` - **Show All Fission Pods:** Identify the pods associated with Fission, including builders, executors, and routers. ```bash kubectl get pods -n fission ``` - **Tail Builder Logs:** Essential for debugging dependency installation or build issues. Replace `` with the actual name. ```bash # Find builder pod name kubectl get pods -n fission -l fission.io/function=builder -o jsonpath='{.items[0].metadata.name}' # Tail logs kubectl logs -f -n fission $(kubectl get pods -n fission -l fission.io/function=builder -o jsonpath='{.items[0].metadata.name}') ``` Or, if you know the name of a specific builder pod (e.g., `buildermgr-786556886-8qm5v`): ```bash kubectl logs -f -n fission buildermgr-786556886-8qm5v ``` - **View Previous Container Logs:** If a pod crashes and restarts, you can inspect the logs from its previous instance. ```bash kubectl logs -n fission --previous ``` - **Delete All Fission HTTP Triggers (in a specific namespace, usually `default` for functions):** ```bash kubectl delete httptriggers --all -n default ``` - **Delete All Fission Functions (in a specific namespace):** ```bash kubectl delete functions --all -n default ``` - **Delete All Fission Packages (in a specific namespace):** ```bash kubectl delete packages --all -n default ``` - **Delete All Fission Resources (HTTP Triggers, Functions, Packages in a specific namespace):** ```bash kubectl delete httptriggers --all -n default kubectl delete functions --all -n default kubectl delete packages --all -n default ``` _Note: Always be careful when using `--all` and ensure you are in the correct namespace to avoid unintended deletions._ --- ## Unit Testing Bots import ExampleCode from '!!raw-loader!@site/../..//examples/medplum-demo-bots/src/finalize-reports.test.ts'; import ExampleCode2 from '!!raw-loader!@site/../..//examples/medplum-demo-bots/src/deduplication/find-matching-patients.test.ts'; import ExampleCode3 from '!!raw-loader!@site/..//examples/src/bots/unit-testing-bots.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; Unit testing your Bot code is crucial to ensuring accurate data and workflows. This guide will go over the most common unit testing patterns. Medplum provides the [`MockClient`](https://github.com/medplum/medplum/blob/main/packages/mock/src/client.ts#L68) class to help unit test Bots on your local machine. You can also see a reference implementation of simple bots _with tests_ in our [Medplum Demo Bots](https://github.com/medplum/medplum-demo-bots) repo. ### Set up your test framework The first step is to set up your test framework in your Bots package. Medplum Bots should work with any TypeScript/JavaScript test runner, and the Medplum team has tested our bots with [Jest](https://jestjs.io/) and [Vitest](https://vitest.dev/). Follow the instructions for your favorite framework to set up your package. Next you'll want to index the FHIR schema definitions. To keep the client small, the `MockClient` class only ships with a subset of the FHIR schema. Index the full schema as shown below, either in a [`beforeAll` function](https://vitest.dev/api/#beforeall) or [setup file](https://vitest.dev/config/#setupfiles), to make sure your [test queries](#query-the-results) work. {ExampleCode} Our [Medplum Demo Bots](https://github.com/medplum/medplum-demo-bots) repo also contains recommended [eslintrc](https://github.com/medplum/medplum-demo-bots/blob/main/.eslintrc.json), [tsconfig](https://github.com/medplum/medplum-demo-bots/blob/main/tsconfig.json), and [vite.config](https://github.com/medplum/medplum-demo-bots/blob/main/vite.config.ts) settings for a faster developer feedback cycle. ### Write your test file After setting up your framework, you're ready to write your first test file! The most common convention is to create a single test file per bot, named `.test.ts`. You will need to import your bot's `handler` function, in addition to the other imports required by your test framework. You'll call this `handler` from each one of your tests. ```typescript import { handler } from './my-bot'; ``` ### Write your unit test Most bot unit tests follow a common pattern: 1. Create a Medplum `MockClient` 2. Create mock resources 3. Invoke the handler function 4. Query mock resources and assert test your test criteria The [finalize-report tests](https://github.com/medplum/medplum-demo-bots/blob/main/src/finalize-report.ts) are a great example of this pattern. #### Create a `MockClient` The Medplum `MockClient` class extends the `MedplumClient` class, but stores resources in local memory rather than persisting them to the server. This presents a type-compatible interface to the Bot's handler function, which makes it ideal for unit tests. {ExampleCode} We recommend creating a `MockClient` at the beginning of each test, to avoid any cross-talk between tests. :::warning[Caution] The MockClient does not yet perfectly replicate all functionality of the `MedplumClient` class. Here are the known differences: - Chained search - Search `_include` and `_revinclude` parameters - Full compliance for token search parameters - More accurate schema validation - **Authentication** - **Most FHIR $ operations** - **`$validate`** - **`Bot/$execute`** - **Terminology service** (e.g., `ValueSet/$expand`) - **FHIRcast** - **Websocket subscriptions** - **Project admin calls** (new project, invite user, create client application, etc.) The Medplum team is working on bringing these features to parity as soon as possible. For related discussion, see [related GitHub issue](https://github.com/medplum/medplum/issues/6889). ::: #### Create test data Most tests require setting up some resources in the mock environment before running the Bot. You can use `createResource()` and `updateResource()` to add resources to your mock server, just as you would with a regular `MedplumClient` instance. The [finalize-report bot](https://github.com/medplum/medplum-demo-bots/blob/main/src/finalize-reports.test.ts) from [Medplum Demo Bots](https://github.com/medplum/medplum-demo-bots/) provides a good example. Each test sets up a [Patient](/docs/api/fhir/resources/patient), an [Observation](/docs/api/fhir/resources/observation), and a [DiagnosticReport](/docs/api/fhir/resources/diagnosticreport) before invoking the bot.
Example: Create Resources {ExampleCode}
##### Creating Rich Test Data in Batches Creating individual test resources can be time consuming and tedious, so the `MockClient` also offers the ability to use batch requests to set up sample data. See the [FHIR Batch Requests guide](/docs/fhir-datastore/fhir-batch-requests) for more details on batch requests. The `MockClient` offers the `executeBatch` helper function to easily execute batch requests and works in the same way that the standard Medplum Client does. The below example is from the [find matching patients bot tests](https://github.com/medplum/medplum/blob/main/examples/medplum-demo-bots/src/deduplication/find-matching-patients.test.ts). Additionally, you can view the [patient data here](https://github.com/medplum/medplum/blob/main/examples/medplum-demo-bots/src/deduplication/patient-data.json).
Example: Creating a large set of patient data with a batch request {ExampleCode2}
In this example we create the `MockClient`, then the test data by calling `executeBatch` in the `beforeEach` function. The `beforeEach` function is an optimization that will run before each test, so you do not need to create the data as a part of every test. Once you have created your data, you can write your tests. The above example uses test contexts, a feature of the [Vitest test framework](https://vitest.dev/guide/test-context.html#test-context). It allows you to pass in the MockClient `medplum` as part of your test context. This test is checking that a `RiskAssessment` was created when looking for potential duplicate patients. ##### Using the Medplum CLI If you have a dev project that already has rich data, you can use the Medplum CLI to easily convert this data into test data. The Medplum CLI offers the optional `--as-transaction` flag when using the `medplum get` command. A `GET` request returns a `Bundle` with `type=searchset`, but this flag will convert it to `type=transaction`.
Example: Get a patient and all encounters that reference them as a transaction {ExampleCode3}
This example searches for all `Patient` resources named 'Alex'. It also uses the [`_revinclude` parameter](/docs/search/includes) to search for all `Encounter` resources that reference those patients. A transaction `Bundle` can be used directly in a batch request, and can be passed as an argument to `executeBatch` on your `MockClient`. This allows you to easily create test resources from already existing data. ##### Cloning an Existing Projects If you want to clone an existing project into a new environment, you can use the `$clone` operation. For more details [see the Projects guide](/docs/access/projects#cloning-and-expunging-projects). #### Invoke your Bot After setting up your mock resources, you can invoke your bot by calling your bot's handler function. See the ["Bot Basics" tutorial](/docs/bots/bot-basics#editing-a-bot) for more information about the arguments to `handler` {ExampleCode} #### Query the results Most of the time, Bots will create or modify resources on the Medplum server. To test your bot, you can use your `MockClient` to query the state of resources on the server, just as you would with a `MedplumClient` in production. To check the bot's response, simply check the return value of your `handler` function. The after running the Bot, the [finalize-report bot's tests](https://github.com/medplum/medplum-demo-bots/blob/main/src/finalize-reports.test.ts) read the updated `DiagnosticReport` and `Observation` resources to confirm their status.
Example: Query the results {ExampleCode}
:::note[A note on idempotency] Many times, you'd like to make sure your Bot is [idempotent](https://en.wikipedia.org/wiki/Idempotence#Computer_science_meaning). This can be accomplished by calling your bot twice, and using your test framework's `spyOn` functions to ensure that no resources are created/updated in the second call.
Example: Idempotency test {ExampleCode}
::: #### Using the Medplum CLI If you have a dev project that already has rich data, you can use the Medplum CLI to easily convert this data into test data. The Medplum CLI offers the optional `--as-transaction` flag when using the `medplum get` command. A `GET` request returns a `Bundle` with `type=searchset`, but this flag will convert it to `type=transaction`.
Example: Get a patient and all encounters that reference them as a transaction ```cli // medplum get --as-transaction 'Patient?name=Alex&_revinclude=Encounter:patient' ```
This example searches for all `Patient` resources named 'Alex'. It also uses the [`_revinclude` parameter](/docs/search/includes) to search for all `Encounter` resources that reference those patients. A transaction `Bundle` can be used directly in a batch request, and can be passed as an argument to `executeBatch` on your `MockClient`. This allows you to easily create test resources from already existing data. --- ## Building on Medplum with AI Coding Agents import MedplumRules from '@site/src/components/MedplumRules'; Agentic engineering (a.k.a. "vibe coding") means using an LLM or agent – Cursor, Claude, Copilot, and others – to build features on Medplum. The biggest lever for quality is making your AI tools learn from Medplum's open-source code and docs rather than generic FHIR knowledge from the open internet. > Building AI _into_ your application at runtime, such as clinical summarization, agents > acting on patient data, or MCP-driven workflows, is a different topic; see > [Build with AI on Medplum](/docs/ai). This page is about using AI to _develop_ on > Medplum. :::caution[AI gets things wrong; review its output] Agents are fast and often right, but they hallucinate fields, mix FHIR versions, and produce plausible-but-wrong code. Treat everything an agent writes as a draft to review, not a finished answer. Apply extra scrutiny to anything that affects security or data exposure, especially access policies, ProjectMembership scopes, and PHI handling. ::: ### Start Here: Plan Your Application Cloning Medplum gives your agent reference material. Use this first prompt to connect that material to your project and start a planning conversation. You can extend an existing application, customize [Medplum Provider](/docs/provider#building-a-custom-ehr), or build a custom application using Medplum's SDKs and components, for example with the [Hello World starter](/docs/tutorials/medplum-hello-world). If undecided, your agent can help you choose a starting point during planning. You can fork and customize Provider while continuing to use Medplum's hosted backend; running your own Provider app does not require self-hosting the Medplum server. 1. Open your application's folder if you have one. Otherwise, you can start planning directly in the Medplum clone before choosing a starting point or application folder. Choose a folder before saving application files. Your application's instructions and decisions belong there; the Medplum clone is a reference. 2. Give the agent access to your Medplum clone. The prompt assumes it is linked as `medplum-link`, as described in [section 1](#1-give-your-ai-tools-access-to-medplum). Otherwise, replace `medplum-link` with the clone's actual path. 3. Copy this prompt and replace the application description: ```text Help me plan [describe your application or feature] on Medplum. Read and follow medplum-link/packages/docs/static/ai/planning.md (or https://www.medplum.com/ai/planning.md), including its Medplum rules. Ask before creating or changing this app's project instruction files. Start with my existing requirements, code, and decisions, then select the relevant decision guides. Ask about consequential missing requirements; do not invent answers. Save and summarize the decisions in my app's repo before implementation. ``` The agent should read and apply the rules immediately. Initial planning does not require an application folder or persistent rules setup. Once your application's folder is known, the agent should verify existing setup or offer to connect the rules to your `AGENTS.md`, `CLAUDE.md`, or equivalent, showing the exact proposed change and asking permission. Planning can continue while setup is pending, and you can decline and continue the work. See the [setup details](#5-encode-your-conventions-in-a-rule-file) for an example. Without a clone, use the [planning instructions](pathname:///ai/planning.md) through an agent with web access. For one workflow, use the **Plan with this guide** prompt on its [decision guide](/docs/decision-guides); it uses the same setup and planning procedure. ### Our Recommendations 1. Give your AI tools access to Medplum: clone the repo and link it in (preferred), give the agent docs and GitHub search, or connect the MCP server. 2. Plan with Medplum's decision guides: have the agent interview you about your requirements, explain the tradeoffs, and save the decisions in your application's repo before implementation. 3. Keep conversations short and re-anchor on the docs: context decays over long sessions, so scope one task per thread and re-check work at checkpoints. 4. Verify and validate the output: check generated code against the type system and tests, review sensitive code by hand, and use Medplum's server-side validation where you can. 5. Encode your conventions in a rule file: a `CLAUDE.md`, Cursor rules, or `AGENTS.md`-style file makes Medplum conventions and your saved decisions available to later tasks. ### 1. Give Your AI Tools Access to Medplum Medplum is open-source with extensive docs on healthcare implementation patterns. When an LLM indexes on Medplum directly, results are significantly better; it pulls from valid snippets, real implementations, and trade-off guidance instead of guessing. #### Option 1: Clone and Link the Repo (Preferred) Agnostic to your LLM and editor: 1. Clone the repo: [https://github.com/medplum/medplum](https://github.com/medplum/medplum) 2. From your repo's top level, symlink your local copy (macOS or Linux): ```bash ln -s /absolute/path/to/medplum medplum-link ``` Alternatively, give your agent the clone's actual path and adjust the prompts on this page. Keep machine-specific symlinks out of version control; provide the checkout separately in new development or automated environments. 3. Tell the model it can read `medplum-link/`. The clone includes the technical docs, reusable components, and example apps. The planning workflow uses: - `medplum-link/packages/docs/docs/decision-guides/`: questions and design tradeoffs. - `medplum-link/packages/docs/static/ai/planning.md`: instructions for planning and implementation. - `medplum-link/packages/docs/static/ai/medplum-rules.md`: reusable rules, shown in [section 5](#5-encode-your-conventions-in-a-rule-file). You can read these files without installing or running the Medplum monorepo. Confirm that your agent can read them before starting. If you open the Medplum clone directly, its root `AGENTS.md` directs supported agents to this guide and the rules, and asks them to offer setup in your application's folder. When working in a separate application folder, use the [starting prompt](#plan-your-application) or [configure its project instructions](#5-encode-your-conventions-in-a-rule-file); access to a reference clone alone does not ensure its instructions are loaded. #### Option 2: Search Docs and GitHub If you'd rather not clone, give your agent search access; for example, one Claude Skill (or Cursor equivalent) that searches the [public docs](/docs) via Algolia, and one that searches the GitHub repo via the GitHub CLI (`gh`). #### Option 3: MCP Server Medplum offers an MCP server for structured access to docs and data. See [Direct Data Access](#direct-data-access-advanced) below, where the setup link and data-access caveats live. ### 2. Plan with Medplum's Decision Guides Use the [Decision Guides](/docs/decision-guides) to work through application design before writing features. Each guide connects discovery questions to Medplum patterns and technical docs. Your agent should ask you about missing requirements, explain the tradeoffs, and save the answers for later coding tasks. Use the [starting prompt](#plan-your-application) to begin. The examples below show what the agent saves and how to use it in later development. #### Keep the Answers in Your Repo The agent saves a short application plan and decisions organized by guide section in your repo, reusing existing planning files. For a focused feature, a single decision record may be enough. The [planning instructions](pathname:///ai/planning.md) define the file layout and how to distinguish agreed choices, recommendations awaiting a decision, and unanswered questions. Keeping these alongside code lets later tasks use them without relying on chat history.
Example: Saved Charting Decisions After a charting discussion, part of `docs/decisions/charting.md` might look like this. These are illustrative answers, not defaults for every application: ```markdown # Charting decisions Source: https://www.medplum.com/docs/decision-guides/charting Local source: medplum-link/packages/docs/docs/decision-guides/charting.md Source revision: [commit from git -C medplum-link rev-parse HEAD] Consulted: [date] ## 2.1 What visit types do you provide, and what does each entail? Status: confirmed Decision: The first release supports an initial visit and a follow-up visit. Reason: Clinical operations confirmed these are the launch workflows. Implementation: Define a Visit Template for each visit type. ## 2.3 How will form responses be parsed into queryable data? Status: proposed Proposal: Use SDC extraction for the initial visit's structured form. Reason: The described mappings appear straightforward; scoring or external lookups would change this choice. Pending: Confirm the form's processing requirements with the application owner before implementing extraction. ## 2.4 What are the signing requirements? Status: open Question: Does this practice require a co-signature for any visit type? Blocks: Finalizing the signing workflow and its acceptance tests. ## 2.5 Who owns and edits Visit Templates? Status: confirmed Decision: Clinical operations specifies changes; engineering implements them. Reason: Clinical operations expects infrequent template changes in the first release. Implementation: Keep templates under code review; a custom template editor is out of scope. ``` A later request to add a template editor changes the saved ownership decision. The agent should identify any remaining questions, such as who can publish template changes, and update the record alongside the implementation.
When choosing implementations, have the agent inspect Medplum's reusable components and examples. [Medplum Provider](/docs/provider) is a useful reference for clinical workflows; you can also build a custom app with the [React components](/docs/api/react). #### Implement from the Saved Decisions For the next coding task, give your agent a specific feature and the saved context. For example, after agreeing on your charting plan: ```text Implement [the next charting feature]. Read my project instructions, application plan if present, and docs/decisions/charting.md. Follow the implementation steps in medplum-link/packages/docs/static/ai/planning.md (or https://www.medplum.com/ai/planning.md) and the relevant technical docs. Ask about missing requirements; do not invent answers. Check the result against the saved decisions and agreed acceptance criteria. ``` Add pointers to these files in the [project rules](#5-encode-your-conventions-in-a-rule-file) your tool loads so future tasks can find them without relying on chat history. For an organization's automated coding system, provide the same Markdown instructions, relevant guides and technical docs at a pinned revision, and saved decisions to each job. Route blocking questions back to the application owner. This workflow does not depend on a particular editor or a single ongoing chat. ### 3. Keep Conversations Short and Re-Anchor on the Docs Even when an agent starts with the right Medplum docs and code in context, quality degrades over a long session. As the conversation grows, the model compacts or drops earlier context, so the precise FHIR patterns and code references it loaded early get summarized away, and it drifts back toward generic FHIR knowledge from the open internet. The result: strong early answers, then subtle regressions later in the same thread. Practical habits that keep the agent grounded: - Scope one task per conversation. Finish a feature or fix, then start fresh rather than letting one thread sprawl across unrelated work. - Re-pull the docs at checkpoints, not just at the start. Have the agent re-read the relevant docs and code after meaningful steps; for example, after it generates a FHIR resource, tell it to check that resource against the docs before moving on. Periodic re-grounding catches drift before it compounds. - Plan, then execute in a clean thread. Use a planning conversation to settle on the pattern (section 2), then open a new conversation with the application plan, relevant decision records, and technical docs. Ask the agent to implement against those decisions and check the result against the acceptance criteria. - Watch for drift. If the agent stops citing real Medplum snippets and starts producing plausible-but-generic FHIR, that's your signal to reset. Keep a pointer to the saved decisions in your project's rule file, and make sure new tasks load the relevant records. Instruction-loading behavior varies by tool; explicitly reload the relevant docs and decisions after compaction. See [section 5](#5-encode-your-conventions-in-a-rule-file). ### 4. Verify and Validate the Output An agent's confidence is not a measure of correctness. Build verification into your workflow so mistakes are caught by tooling and review, not in production. #### Easy Checks (No Data Access Needed) These run against your own project and the cloned repo, with no server or live data: - Type-check and compile. The single best check, and one the agent can run itself if it has terminal access: build the agent's code against the `@medplum/fhirtypes` types and run `tsc`. The types are strict, so a hallucinated field on a _typed_ FHIR resource (e.g. `const patient: Patient = { ... }`) is a compile error rather than a runtime surprise. The check only bites when the agent actually annotates its resources; untyped object literals slip through. - Run the existing tests, lint, and build. Regressions and bad search params fail loudly. - Self-review against the docs. Have the agent re-read the relevant doc or example and compare its output before you accept it. - Mind the FHIR version. Medplum uses FHIR R4; LLMs readily mix in R5 fields or deprecated APIs from their training data, so this is a common silent error. A project rule file (see section 5) is the durable place to pin this. - Review sensitive output by hand. Access policies, auth scopes, and any code touching PHI deserve a detailed human read before shipping. A second pass, where the agent re-checks its own work against the docs in a fresh conversation, catches more but does not replace human review. #### Going Further (Requires a Server) Full FHIR validation runs server-side. Submit resources to the FHIR [`$validate`](/docs/api/fhir/operations/validate-a-resource) operation, validate codes against a ValueSet with [`$validate-code`](/docs/api/fhir/operations/valueset-validate-code), or round-trip resources through a `MedplumClient`; these check against the real FHIR and profile definitions, catching structural and terminology errors the type system can't (hallucinated LOINC, SNOMED, and ICD codes are a common failure mode). They need either a local dev server (synthetic data, no BAA required) or a token against a live server; see [Direct Data Access](#direct-data-access-advanced) for the caveats. ### 5. Encode Your Conventions in a Rule File The agent should read and apply the Medplum rules immediately, including during initial planning before you have chosen an application folder. Once that folder is known, check whether the project instructions your coding tool loads already include or reference those rules. If so, verify that the rules can be read instead of adding another copy. Otherwise, offer to connect them for future sessions. Initial planning can continue while the folder is undecided or setup is pending. This setup also applies when you are starting development without a planning session. The agent must show the proposed filename and exact addition, then ask permission before creating or changing project instructions. Approval already given for the same change is sufficient. Preserve existing instructions, verify the agreed changes, and report what was configured. If you decline, cannot respond, or the files cannot be edited, continue the requested work without changing those instructions or claiming setup is complete. Do not repeat a declined offer in the same session. With a local clone, the default is a short reference to its rules file. For manual setup, merge a section like this into your application's `AGENTS.md`, `CLAUDE.md`, or equivalent rule file, adjusting the clone path: ```markdown ## Medplum Before planning or implementing Medplum features, read and follow `medplum-link/packages/docs/static/ai/medplum-rules.md`. If the clone is unavailable, use https://www.medplum.com/ai/medplum-rules.md. ``` This explicitly tells the agent to open the file; a standalone `medplum-rules.md` is not automatically discovered. If you prefer a self-contained application repo, copy the rules there and reference that copy instead. Record its source and revision or retrieval date so you can refresh it when updating Medplum. After plans and decisions are saved, the agent can propose adding their actual paths to your project instructions, using the same review and permission process. If you declined setup in this session, it should include those paths in its handoff without repeating the offer. Reference only files that exist; keep the full plan and guides in their source files. Check your tool's instruction-loading behavior: see [Codex's AGENTS.md guidance](https://learn.chatgpt.com/docs/agent-configuration/agents-md) or [Claude Code's project instructions](https://code.claude.com/docs/en/memory). After setup, start a fresh session and ask the agent to read the project's Medplum rules and identify the saved planning files, if present. Check the paths and its file reads. If references are not followed reliably, include the rules below directly in the loaded project instructions. The example below is rendered directly from the reusable [medplum-rules.md](pathname:///ai/medplum-rules.md) file, also available at `medplum-link/packages/docs/static/ai/medplum-rules.md` in the clone. For agents using web access, the public URL is `https://www.medplum.com/ai/medplum-rules.md`. The rules prefer the local clone and fall back to the public docs. Keep the full guides and planning procedure in their source files so the agent can read them when needed. ### Direct Data Access (Advanced) Giving your AI tool direct access to your Medplum data, via an API access token or the MCP server (setup: [MCP](/docs/ai/mcp)), speeds things up but carries real caveats: :::caution - If it can reach live PII/PHI: you _must_ have a BAA with your LLM vendor. - If you don't have a BAA: apply an access policy so the token reaches only non-PII/PHI data. ::: In FHIR, only request and record resources generally contain PII/PHI; templating resources define their _shape_ but hold no sensitive data ([workflow patterns](/blog/fhir-workflow-patterns-to-simplify-your-life)). Scope the token via an [access policy](/docs/access/access-policies), typically on its [ProjectMembership](/docs/api/fhir/medplum/projectmembership), and, per the caution at the top of this page, review that policy by hand before you trust it. ### See Also - [Build with AI on Medplum](/docs/ai) – building AI into your application at runtime - [MCP](/docs/ai/mcp) – connect Medplum's MCP server - [Access Policies](/docs/access/access-policies) – scope what a token can reach - [Validate a Resource](/docs/api/fhir/operations/validate-a-resource) FHIR operation - [Medplum Provider](/docs/provider) – reference implementation for clinical workflows --- ## Automating Care Workflows Once a coordinator completes intake, the review Task should appear without someone remembering to create it. That's a good candidate for automation. The less obvious part is making sure the same event doesn't create three review Tasks when it is retried. Start by defining the owners, completion evidence, and exception paths. Medplum Bots and Subscriptions execute your rules; FHIR resources retain the state those rules act on. ### Choose What Starts the Work | Trigger | Implementation | Identity for generated work | | --- | --- | --- | | Staff action | Application creates a Task | Stable action/submission identifier | | Resource transition | Scoped Subscription invokes a Bot | Source resource plus step and relevant occurrence | | Admission or discharge event | HL7 integration maps the event to FHIR; a Bot creates the follow-up work | Source encounter plus the intended follow-up occurrence | | Recurring follow-up | Scheduled Bot evaluates due occurrences | Source plan/case plus step and occurrence date or sequence | | Reusable clinical protocol | Apply PlanDefinition and ActivityDefinition | Track each intended application and inspect its generated resources | For example, an [HL7 ADT discharge message](/docs/integration/hl7-interfacing/adt) can lead to a follow-up Task after the integration resolves the patient and encounter. Define which discharge event starts the work and how corrections or cancellations affect it. Preserve source message identifiers for ingestion and a stable encounter/step key for the follow-up. Use local work-type and stage CodeSystems consistently. An identifier is an identity key, not the current stage; it must remain stable across retries. ### Turn a Protocol into Patient Work [Clinical Protocols](/docs/careplans/protocols) describes authoring PlanDefinition and ActivityDefinition. Medplum's [`PlanDefinition/$apply`](/docs/api/fhir/operations/plandefinition-apply) creates a patient CarePlan whose activity references a RequestGroup. RequestGroup actions reference generated Tasks; supported ActivityDefinitions can also produce a ServiceRequest. ```mermaid flowchart LR definition[PlanDefinition] -->|apply for a patient| plan[CarePlan] plan -->|activity.reference| group[RequestGroup] group -->|action.resource| task[Task] ``` The generated RequestGroup sits between the plan and its Tasks. Follow those references when building the patient's work list. Inspect generated Tasks' focus, inputs, owners, status, and patient context. Configure supported task-element extensions where appropriate. Applying a definition is not a guarantee that arbitrary FHIR conditions, repetition rules, or temporal dependencies will execute. Verify the supported subset against the deployed Medplum version and implement the remaining orchestration explicitly. :::caution[A timeout does not mean nothing was created] Do not blindly retry `$apply` after a timeout: it can create another set of resources. Reconcile the previous application's outcome before retrying. Applications that need a fully idempotent launch should design the launch identity and resource creation workflow explicitly; this is separate from reusable protocol authoring. ::: ### React to the Change That Matters Subscribe to the resource type and work type you intend to handle. For preparation becoming completed, use the Medplum interaction and FHIRPath criteria extensions to restrict execution to updates where status changes to the required state. Handle creation separately if it should trigger work; `%previous` is absent then. The [Subscription Extensions guide](/docs/subscriptions/subscription-extensions) defines these Medplum-specific features. Re-check the current source resource in the Bot before acting: another update may have cancelled or replaced the work since the event occurred. ### Make the Second Run Safe If the same event arrives twice, both runs should lead to the same work item. Use conditional create keyed by `identifier`, such as `createResourceIfNoneExist`, for generated work. Do not search and then create: concurrent runs can both observe no match. Use version checking when advancing an existing Task so a stale event cannot overwrite a later decision. When linked writes must succeed together, use a transaction Bundle with conditional creates and version-checked updates. A transaction covers FHIR writes; it cannot roll back an external fax, email, or payer submission. :::tip[Track the send as well as the Task] For external side effects, persist the intended operation and correlation identifier, use the provider's idempotency mechanism when available, and reconcile uncertain outcomes before resending. Reusing a Task does not prevent duplicate delivery by itself. ::: ### Create the Next Follow-Up When It Is Due Use a [scheduled Bot](/docs/bots/bot-cron-job) to evaluate eligible plans or cases and create each due occurrence once. Define the calendar, time zone, missed-run catch-up policy, and stop conditions. Re-check eligibility before creation so a closed case does not keep generating work. For overdue work, update or escalate the existing item rather than creating a new copy of the obligation. See [Handoffs and Escalation](/docs/careplans/handoffs-and-escalation) for deadline semantics and paused clocks. ### Let the Assignee Know Use [WebSocket subscriptions](/docs/react/use-subscription) to refresh an open work queue when Tasks are assigned, changed, or escalated. On reconnect, query the current queue so the UI catches changes that happened while it was offline. When staff need an alert outside the application, use a scoped Subscription and Bot to invoke the configured notification integration. Define the recipient, channel, and a stable notification key for the assignment or escalation event. Keep sensitive details in the authenticated application, and let staff reach the Task from the notification. The Task remains the work record even if delivery fails; monitor and retry notification delivery separately. ### Give Failed Runs a Recovery Path Monitor Bot failures and maintain a recovery queue with source identifiers and the failed step. Bot-channel Subscriptions do not use the external rest-hook retry policy; implement the recovery behavior you require. See [Bots in Production](/docs/bots/bots-in-production). Test duplicate events, out-of-order events, competing claims, a cancelled source, and partial external failure. Run Bots under access policies appropriate to their work, and preserve the distinction between the software actor and clinical requester. When a protocol changes, decide whether existing patients retain their original plan or receive an explicit reviewed migration. Record the definition/version used where the chosen canonical model supports it. A new definition version should not silently reinterpret completed work. --- ## Care Plans, Goals, and Progress A work queue tells the team what to do next. A CarePlan helps explain how those activities fit into the patient's care: what they are working toward, which services are planned, and how they will assess progress. Add one when people need to review goals and activities together. Independent work can stay in a Task queue. ### Start with What the Patient Is Working Toward | Field | Use | | --- | --- | | `subject` | Patient receiving the planned care | | `status` and `intent` | Whether the plan is draft, active, or ended, and whether it is a proposal, plan, or order | | `addresses` | Conditions the plan addresses | | `goal` | References to independently tracked Goals | | `careTeam` | Teams expected to participate | | `activity.reference` | Requests, Tasks, or a RequestGroup describing intended activities | | `supportingInfo` | Context used in forming the plan, including an EpisodeOfCare under the convention in the longitudinal guide | `CarePlan.encounter` records the encounter associated with plan creation. It is not a substitute for the longitudinal association in [case tracking](/docs/careplans/longitudinal-patient-case-tracking). :::tip[Give an activity its own resource when it needs one] Choose either `activity.reference` or `activity.detail` for an activity; R4 does not allow both on the same activity. Prefer a referenced resource when the activity needs independent assignment, search, or lifecycle. Use inline `detail` for a simple planned activity that does not yet need its own resource. ::: ### Give Planned Activities a Place in the Queue For a manually authored plan, reference an operational Task from `CarePlan.activity.reference`. Use `Task.basedOn` for the plan that authorizes the work and `Task.focus` for the specific request or resource being acted on. Keep `Task.for` populated for patient work. A clinical order should have its own request resource. A CarePlan may reference a ServiceRequest, while several Tasks act on that request for scheduling, preparation, or review. Avoid listing both an order and every fulfillment Task as independent clinical activities if this would count the same intended service multiple times. Plans generated by Medplum's `$apply` use a RequestGroup between CarePlan and Tasks. Understand the generated graph rather than assuming it matches the manually authored pattern. See [Automating Care Workflows](/docs/careplans/automating-workflows). ### Make Progress Something You Can Assess A goal should let the team answer whether the patient is making progress. Each Goal has its own `subject`, `description`, and `lifecycleStatus`. Use `target` to define the measure, desired value, and due date or duration. A numeric target uses `detailQuantity` with a meaningful unit; a qualitative target can use another permitted detail type. The clinical team chooses the target and validated terminology. Do not infer a clinical goal from a workflow deadline or reuse an illustrative threshold as clinical guidance. `achievementStatus` describes progress toward the goal. It is distinct from `lifecycleStatus`: a goal can be ended without being achieved. Link supporting Observations through `outcomeReference`. That field accepts Observations in R4; it does not accept arbitrary documents or QuestionnaireResponses. When an assessment supports a goal, retain the QuestionnaireResponse and use the chosen [extraction approach](/docs/questionnaires/parsing-questionnaire-responses) to produce the appropriate Observation. Choose one extraction strategy per form to avoid duplicate downstream records. Clinical evidence and its interpretation remain distinct. ### Update the Plan and Check the Open Work Use resource version history for routine edits to an existing plan. When a new plan replaces a prior completed or terminated plan, link the new plan's `replaces` to the old one and set the old lifecycle explicitly. Use `partOf` for a real broader plan; it does not mean revision or replacement. :::caution[A plan change can leave old work in the queue] Suppose a clinician changes the follow-up schedule. The old outreach Task may still be sitting in a coordinator's queue. Review open Tasks when changing the plan. Decide which remain valid, which should be cancelled with a reason, and which require replacement. Neither editing a CarePlan nor updating a PlanDefinition migrates existing work automatically. ::: ### Close Each Record When Its Work Is Done Close an activity when its work and evidence meet the completion criteria. Update each Goal according to its clinical assessment, and close the CarePlan when its scope has ended. Close an EpisodeOfCare only when the period of responsibility ends. A completed Task does not automatically achieve a Goal or finish a case. See the [R4 CarePlan definitions](https://hl7.org/fhir/R4/careplan-definitions.html) for activity constraints and [Operational Reporting](/docs/careplans/operational-reporting) for measuring work and outcomes separately. --- ## Clinical Decision Support and Follow-Through A recommendation is useful only if it reaches someone who can act on it. Sometimes a link in the chart is enough. Sometimes the recommendation needs an owner and follow-up that will still be there when the chart is closed. Clinical decision support (CDS) can provide reference material, rule-based recommendations, or predictive results. This guide covers how to bring that guidance into a care workflow and track the work it creates. ### Meet the Clinician Where the Decision Happens | Need | Approach | | --- | --- | | Contextual reference material | Link from the relevant chart context; record follow-up only when the workflow requires it | | Guidance during chart review or ordering | Use a supported CDS Hooks interaction, embedded application, or the service's documented API | | Background identification of work | A Bot evaluates scoped patient data and creates an appropriately assigned review Task | | Follow-through after review | Persist the authorized request and separately track its fulfillment | Use [CDS Hooks](/docs/integration/cds-hooks) for the discovery, request, prefetch, and card contract. Use [SMART App Launch](/docs/integration/smart-app-launch) when launching an external application. These integration choices do not determine the clinical approval policy. ### Show the Guidance and Its Evidence For a Medplum-hosted CDS Hooks service, configure the Bot's `cdsService` hook and prefetch requirements. The client invokes the service at the intended workflow point and displays returned cards. It must implement the user interaction for suggestions and proposed actions. :::caution[Displaying a suggestion does not authorize an order] A returned card is not automatically a persisted clinical request or Task. Check proposed resources against FHIR R4, the receiving application, and the user's authority before committing a clinical action. Preserve the clinician's decision separately from the service's evaluation result. ::: Display the guidance source and relevant evidence. Handle unavailable services and incomplete context explicitly. A failure can create a manual review obligation when the clinical workflow requires one; it should not silently appear as a successful evaluation with no recommendations. ### Turn Recommendations into Review Tasks For recommendations that need follow-through, give the reviewer a work item that survives closing the chart: ```mermaid flowchart TD guidance[Recommendation requiring review] --> task[Assigned review Task] task --> decision{Reviewer decision} decision -->|Accept| request[Persist authorized request] request --> followup[Track fulfillment separately] decision -->|Decline| reason[Record disposition and rationale] decision -->|Defer| later[Record decision and arrange later review] ``` Use a review Task when a recommendation requires acknowledgment, ownership, or follow-up across sessions. Populate `for`, an accountable `owner` or required `performerType`, and the relevant evidence in `focus` or `input`. Use a stable identifier derived from the rule and triggering event or review period so repeated evaluations do not create duplicate open work. Record the decision in a governed local `businessStatus`, with an authored, timestamped rationale in `note` and any resulting clinical request in `output`. A completed review Task means review is finished. The recommendation may have been accepted, declined, or deferred according to the workflow; its clinical service is not thereby complete. If accepted guidance leads to a referral, use a ServiceRequest and fulfillment Tasks as described in [Referrals](/docs/careplans/referrals). If it changes a treatment plan, update the CarePlan through the clinical review path. Preserve patient references on every resulting resource. ### Keep Evaluation Records When You Need Them Suppose a rule runs ten times during a week, but only one result needs review. One review Task may cover the work. Retaining all ten evaluations is a separate requirement. Some applications also need the inputs, rule/model version, evaluation result, and disposition for informational results that never create a Task. FHIR R4 `GuidanceResponse` is a candidate for the evaluation result: it can identify the evaluated module, patient, occurrence time, output Parameters, and a resulting CarePlan or RequestGroup. Its `status` describes evaluation success or failure, not clinician acceptance. A CDS Hooks card does not automatically map to GuidanceResponse, and Medplum's CDS Hooks endpoint does not establish that persistence model for you. :::note[GuidanceResponse is an additional modeling choice] Choose this additional model only after defining retention and retrieval requirements and the mapping for the particular service. If it is adopted, a review Task can reference that evaluation in `focus` or `input`. Keep this choice explicit rather than creating a Task for every informational card. ::: ### Check Whether Guidance Leads to Follow-Through Track who maintains the rules, how versions are introduced, when repeat review is warranted, and how to recover missed work. Measure actionable reviews, accepted and declined recommendations, follow-up completion, and alert burden with clear denominators. Clinical outcomes need separate evidence. Apply [Access Policies](/docs/access/access-policies) to the service context and stored outputs. ### Understand the Certification Context Medplum's [ONC certification documentation](/docs/compliance/onc#criteria-certified) lists the certified product and criteria, including (b)(11) Decision Support Interventions. Use that page and its linked product listing for certification scope. Building a CDS workflow on Medplum still requires evaluating the particular application, intervention, and use case. Under HTI-1, the (b)(11) criterion replaced the legacy (a)(9) CDS criterion. The [ONC Decision Support Interventions guidance](https://healthit.gov/test-method/decision-support-interventions/) describes evidence-based and predictive interventions, source information, feedback, and applicable risk-management requirements. Treat the [Insights Condition](https://healthit.gov/certification-health-it/insights-condition/) as a separate reporting topic with its own applicability and measures; operational Task counts alone do not establish that reporting model. For context-aware reference links, [HL7 Infobutton](https://www.hl7.org/implement/standards/product_brief.cfm?product_id=208) is another integration option. Choose the interaction that fits the clinician's workflow, then confirm the requirements for the product being delivered. See [Automating Care Workflows](/docs/careplans/automating-workflows) for event handling and [Operational Reporting](/docs/careplans/operational-reporting) for transitions and measures. --- ## Defining your Diagnostic Catalog import ExampleCode from '!!raw-loader!@site/..//examples/src/careplans/diagnostic-catalog.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; A **diagnostic catalog** contains all the pertinent information about the diagnostic services you provide, including your analytes, reference ranges, panels, specimen requirements, and laboratory procedures. Having a well-defined, structured catalog enables: - Robust access controls - Higher quality analytics - Smoother [CLIA/CAP certification](/docs/compliance/clia-cap) - [Streamlined billing](/docs/compliance/clia-cap) At the end of this guide, you will understand how to represent your diagnostic catalog in FHIR at a detailed level an example of which is shown in this diagram. ```mermaid graph TD A((Men's Health)) --> TestProc(Testosterone Test) A --> ElecProc(Electrolyte Panel) Z((Women's Health)) --> E2Proc(Estradiol E2 measurement) Z --> ElecProc TestProc --> E(Free Testosterone Level) E2Proc --> W(Estradiol E2 Level) ElecProc --> F(Sodium Level) ElecProc --> G(Potassium Level) ElecProc --> H(Chloride Level) ElecProc --> J(Total CO2 Level) subgraph Services A Z end subgraph Procedures TestProc ElecProc E2Proc end subgraph Analytes E F G H J W end ``` Follow the guide step-by-step to build up the components shown: 1. Define your clinical observations 2. Define your specimens 3. Define your orderable services 4. Define your laboratory procedures Our recommendations are informed by the [Order Catalog Implementation Guide](http://hl7.org/fhir/uv/order-catalog/2020Sep/) implementation guide, which developed with contributions from Labcorp, Quest Diagnostics, and other industry leaders. :::tip[Sample Data] You can download the examples in this guide as a FHIR bundle [here](./sample-diagnostic-catalog.json), and upload them to your project using the [Medplum Batch Upload Tool](/docs/tutorials/importing-sample-data#batch-upload-tool) ::: ### Define your clinical observations The first step in building your catalog is to define which clinical quantities, or "observations", you will measure. In a lab context, an example of a clinical observation would be an an HBA1c percentage. For a given patient, this observation might have a value such as 5.3%. The [`Observation`](/docs/api/fhir/resources/observation) is the primary _operational_ resource used to record a clinical quantity for a specific patient. [`ObservationDefinition`](/docs/api/fhir/resources/observationdefinition) is the corresponding _administrative_ counterpart, and is used to define how an [`Observation`](/docs/api/fhir/resources/observation) should be measured, interpreted, and reported. [`Observations`](/docs/api/fhir/resources/observation) and [`ObservationDefinitions`](/docs/api/fhir/resources/observationdefinition) are linked by sharing a common `code` element, which should include a [LOINC code](./loinc) in most cases. A great [`ObservationDefinition`](/docs/api/fhir/resources/observationdefinition) contains the following: | Element | Description | Code System | Example | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `code` | Code representing the observation type. | LOINC (see [LOINC codes](./loinc)) | [2951-2](https://loinc.org/2951-2) - Sodium [Moles/volume] in Serum or Plasma | | `quantitativeDetails.unit` | Units expressing the observation value. | [UCUM](https://hl7.org/fhir/R4/ucum.html) | mmol/L | | `quantitativeDetails.decimalPrecision` | Number of places of precision _to the right of the decimal point_ | | 2 (e.g. `0.11`) | | `qualifiedInterval` | Range of valid or reference values for the observation, to be used during interpretation. See our [guide on reference ranges](./reference-ranges) for more info. | | Low: <= 134.00 mmol/L Normal: 135.00 mmol/L - 145.00 mmol/L High: >= 146.00 mmol/L | | `preferredReportName` | Patient-friendly name used for reporting the observation result. | | Sodium Level | Key amongst these is the `qualifiedInterval` element, which is used to define _how results should be interpreted._ See our [guide on reference ranges](./reference-ranges) for more information.
Example: Blood Sodium Level {ExampleCode}
### Define your specimens In the context of laboratory use cases, it's essential to recognize that observations are based on samples extracted from patients, known as "specimens". A well constructed diagnostic catalog links the specimen requirements for each test to the test definition to provide lab operators a complete picture of the collection process. The [`Specimen`](/docs/api/fhir/resources/specimen) resource resource refers to a specific specimen, like a tube of blood, usually belonging to a patient. As with [`Observations`](/docs/api/fhir/resources/observation), [`Specimen`](/docs/api/fhir/resources/specimen) has a corresponding _administrative_ resource, called [`SpecimenDefinition`](/docs/api/fhir/resources/specimendefinition). [`SpecimenDefinition`](/docs/api/fhir/resources/specimendefinition) describes the type of specimen material to be collected, as well as details about the collection process, storage, handling, and preparation for testing. The [`SpecimenDefinition`](/docs/api/fhir/resources/specimendefinition) allows you to specify many details about your specimen, but the most relevant are: | Field | Description | Code System | Example | | ----------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `typeCollected` | Type of material collected. | [SNOMED (children of 123038009 - Specimen)](https://browser.ihtsdotools.org/?perspective=full&conceptId1=123038009&edition=MAIN/2023-07-31&release=&languages=en) | [122554006](https://browser.ihtsdotools.org/?perspective=full&conceptId1=122554006&edition=MAIN/2023-07-31&release=&languages=en) - Capillary Blood Specimen | | `collection` | Procedure used for collection. | [SNOMED (children of 118292001 - Removal)](https://browser.ihtsdotools.org/?perspective=full&conceptId1=118292001&edition=MAIN/2023-07-31&release=&languages=en) | [278450005](https://browser.ihtsdotools.org/?perspective=full&conceptId1=278450005&edition=MAIN/2023-07-31&release=&languages=en) - Finger-prick sampling | | `typeTested.container` | Details about the container storing the specimen. | [Specimen Container Type](https://hl7.org/fhir/R4/valueset-specimen-container-type.html) | [467989009](https://browser.ihtsdotools.org/?perspective=full&conceptId1=467989009) - Capillary blood collection tube, no-additive | | `typeTested.handling` | Duration of storage at different temperature ranges | [Handling Condition](https://hl7.org/fhir/R4/valueset-handling-condition.html) | Store refrigerated at 2-8°C for up to 48 hours | | `typeTested.preference` | Whether this collection output is the preferred form, or an alternate | preferred \| alternate | preferred | :::tip[`typeCollected` vs `typeTested`] Material that is collected from a patient may be split up, prepared, and handled different ways. The `typeTested` elements describe _all the potential outputs_ of the collection process, and contains a lot more information about the containment vessel, rejection criteria, and temperature restrictions for _each_ output. :::
Example: Capillary Blood Sample The following examples defines a fingerprick blood sample that is distributed into two collection tubes: a red cap and a green cap. {ExampleCode}
### Define your services The next step is to roll up your individual tests into **orderable services** that your patients can order. These can be thought of as your diagnostic "product offerings," and are commonly known as "panels" in a lab context. These products are represented as [`PlanDefinition`](/docs/api/fhir/resources/plandefinition) resources. The [`PlanDefinition`](/docs/api/fhir/resources/plandefinition) is primarily a grouping resource that stores metadata about the service and references the [`ActivityDefinitions`](/docs/api/fhir/resources/activitydefinition) you will create in the next section. [`PlanDefinition's`](/docs/api/fhir/resources/plandefinition) important fields: | Field | Description | Code System | Example | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `name` | The computer-friendly name of the service. | | mens-health-panel | | `title` | The human-friendly name of the service. | | Men's Health Panel | | `identifier` | Business identifier for the service (i.e. product or SKU code) | | dx-panel-12345 | | `type` | Whether the service is a single test or a panel | [Laboratory service types](http://hl7.org/fhir/uv/order-catalog/2020Sep/ValueSet-laboratory-service-type.html) | panel | | `useContext` | How this [`PlanDefinition`](/docs/api/fhir/resources/plandefinition) should be interpreted. For diagnostic procedures, this is a fixed value: Lab Order Entry | | [Lab Order Entry](https://terminology.hl7.org/1.0.0//CodeSystem-v3-ActCode.html) | | `status` | Whether or not the[`PlanDefinition`](/docs/api/fhir/resources/plandefinition) is active | | active | | `action.code` | The code for lab procedure corresponding | [LOINC](./loinc) | [55231-5](https://loinc.org/55231-5) - Electrolytes panel - Blood | | `action.definitionCanonical` | The "canonical url" of the the [`ActivityDefinition`](/docs/api/fhir/resources/activitydefinition) representing the procedure (see [below](#activitydefinitions)) | | http://example.org/ActivityDefinition/electrolyte-panel | In the next section, we'll learn more about the `PlanDefinition.action` element, which represents the lab procedures used to _fulfill_ the diagnostic service order. ### Define your lab procedures Now that you have represented your service menu as [`PlanDefinitions`](/docs/api/fhir/resources/plandefinition) , you will use [`ActivityDefinition`](/docs/api/fhir/resources/activitydefinition) resource to define your corresponding procedures to fulfill each service. While [`PlanDefinitions`](/docs/api/fhir/resources/plandefinition) are patient facing resources, [`ActivityDefinitions`](/docs/api/fhir/resources/activitydefinition) are primarily used by lab operators to aid them in fulfilling the order. To link the two, each entry in `PlanDefinition.action` references an individual lab procedure, with `PlanDefinition.action.definitionCanonical` referencing an [`ActivityDefinition`](/docs/api/fhir/resources/activitydefinition) resource for details. :::caution[Note: Canonical References] [`PlanDefinitions`](/docs/api/fhir/resources/plandefinition) and [`ActivityDefinitions`](/docs/api/fhir/resources/activitydefinition) are linked via what is known as a _canonical_ reference, not a standard reference as with most other resources. `PlanDefinition.action.definitionCanonical` is a URL string, that must match the `url` field of the [`ActivityDefinition`](/docs/api/fhir/resources/activitydefinition) it references. ::: There is a bit of an art to determining divide the individual tests into procedures, and it requires an understanding of your lab operations. Some considerations to help guide you: - Are there reusable groups of tests that are _always_ performed together? - Do these groups have their own [LOINC](./loinc) codes? - Do your analyzers have a single input to order this group of results? #### ActivityDefinitions The [`ActivityDefinition`](/docs/api/fhir/resources/activitydefinition) resource stores detailed information about each procedure, and is the resource that links `PlanDefinition.action`, to the [`ObservationDefinitions`](/docs/api/fhir/resources/observationdefinition) and [`SpecimenDefinitions`](/docs/api/fhir/resources/specimendefinition) we defined earlier. The most important fields for [`ActivityDefinition`](/docs/api/fhir/resources/activitydefinition) are summarized below: | Element | Description | Code System | Example | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------- | | `code` | The LOINC code corresponding to this procedure. Should match the code used in `PlanDefinition.action` | [LOINC](./loinc) | [55231-5](https://loinc.org/55231-5) - Electrolytes panel - Blood | | `url` | Known as the "canonical URL" for the resource. This should be a fully qualified, globally unique URL. FHIR recommends for many administrative resources (aka "definitional resources") to have canonical URLs to provide a globally unique business identifier. Read more about canonical URLs [here](https://hl7.org/fhir/resource.html#canonical) A recommended pattern for constructing this URL is: `http://[your-company-url]/ActivityDefinition/[test-name]` | | http://example.org/ActivityDefinition/electrolyte-panel | | `observationResultRequirement` | References to the [`ObservationDefinition`](/docs/api/fhir/resources/observationdefinition) resources for the test results produced by this procedure (see above). | [See above](#define-your-clinical-observations) | | | `specimenRequirement` | References to the [`SpecimenDefinition`](/docs/api/fhir/resources/specimendefinition) resources for the test results produced by this procedure (see above). | [See above](#define-your-specimens) | | | `name` | A computer-friendly name for the procedure | | electrolytes-panel-blood-measurement | | `title` | A human-friendly name for the procedure | | Electrolytes panel measurement in blood | | `kind` | The kind of resource that will represent the lab order. For diagnostics, this is always `ServiceRequest`. | | ServiceRequest | | `status` | Whether or not the[`ActivityDefinition`](/docs/api/fhir/resources/activitydefinition) is active | | active | #### The Simple Case In most cases, each service will only require a single laboratory procedure. In these cases, you will only need a single `PlanDefinition.action` and [`ActivityDefinition`](/docs/api/fhir/resources/activitydefinition), representing the main operational procedure performed to fulfill this laboratory service.
Example: Electrolyte Panel In the example below, a patient can order an Electrolyte Panel, which is a single laboratory procedure Patient-facing service {ExampleCode} Laboratory Procedure {ExampleCode}
#### Reusing Procedures In some cases, a product offering might _embed_ multiple procedures that are reused across service offerings. In these cases, we can define multiple entries in `PlanDefinition.action`, each with their own [`ActivityDefinition`](/docs/api/fhir/resources/activitydefinition). This allows you to reuse the data definition of your procedures, while allowing you to compose them into different patient-facing product offerings.
Example: Men's Health Panel This PlanDefinition reuses the Electrolyte Panel from the previous example, and but adds a free testosterone test. Patient-facing service {ExampleCode} Laboratory Procedures Testosterone Procedure {ExampleCode} Electrolytes Procedure {ExampleCode}
:::tip[Sub actions] Beyond flat lists of procedures, FHIR [`PlanDefinitions`](/docs/api/fhir/resources/plandefinition) can be used to represent sub-procedures, mutually exclusive groups of procedures, reflex tests, and other complicated arrangements. These advanced scenarios are out of scope for this guide, but you can check out the [this implementation guide](http://hl7.org/fhir/uv/order-catalog/2020Sep/exlabservices.html) for examples of how these might be implemented ::: ### Querying your catalog You can query all [`PlanDefinitions`](/docs/api/fhir/resources/plandefinition) that represent a laboratory procedure using the and the associated [`ActivityDefinitions`](/docs/api/fhir/resources/activitydefinition) with this query: **Typescript** {ExampleCode} **CLI** {ExampleCode} **cURL** {ExampleCode} Another common query is to get all the [`ObservationDefinition`](/docs/api/fhir/resources/observationdefinition) and [`SpecimenDefinitions`](/docs/api/fhir/resources/specimendefinition) in a single service. Unfortunately, there is currently no way to do this using a single query. However, this can be done in two parts: 1. Query all ActivityDefinitions for a given PlanDefinition using [`_include`](/docs/search/includes#_include-and-_revinclude) directive **Typescript** {ExampleCode} **CLI** {ExampleCode} **cURL** {ExampleCode} 2. For each resulting [`ActivityDefinition`](/docs/api/fhir/resources/activitydefinition), read each [`ObservationDefinition`](/docs/api/fhir/resources/observationdefinition) and [`SpecimenDefinition`](/docs/api/fhir/resources/specimendefinition). This operation is well suited to [GraphQL](/docs/graphql) **GraphQL** {ExampleCode} **TypeScript** {ExampleCode} ### Putting it all together Now we'll put all these concepts together to model a basic lab catalog. The diagram below illustrates the setup we will be modeling: - We have two orderable services, a "Men's Health Panel" and a "Women's Health Panel" - Each service has a shared procedure, the "Electrolyte Panel" - Each service also has a sex-specific procedure: "Testosterone" for men and "Estradiol" for women - The Men's health panel will produce the following clinical measurements: - Free Testosterone Level - Sodium Level - Potassium Level - Chloride Level - Total CO2 Level - The Women's health panel will produce the following clinical measurements: - Estradiol E2 Level - Sodium Level - Potassium Level - Chloride Level - Total CO2 Level ```mermaid graph TD A((Men's Health)) -->|action.definitionCanonical| B(Testosterone Test) A -->|action.definitionCanonical| C(Electrolyte Panel) Z((Women's Health)) --> |action.definitionCanonical| Y(Estradiol E2 measurement) Z -->|action.definitionCanonical|C B --> E(Free Testosterone Level) Y --> W(Estradiol E2 Level) C --> F(Sodium Level) C --> G(Potassium Level) C --> H(Chloride Level) C --> J(Total CO2 Level) subgraph PlanDefinition A Z end subgraph ActivityDefinition B C Y end subgraph ObservationDefinition E F G H J W end ```
Services (PlanDefinition) Men's Health {ExampleCode} Women's Health {ExampleCode}
Procedures (ActivityDefinition) Testosterone {ExampleCode} Estradiol {ExampleCode} Electrolytes {ExampleCode}
Observations (ObservationDefinition) Free Testosterone {ExampleCode} Estradiol {ExampleCode} Sodium {ExampleCode} Potassium {ExampleCode} Chloride {ExampleCode} CO2 {ExampleCode}
Specimen (SpecimenDefinition) Finger-prick Capillary Blood {ExampleCode}
--- ## Handoffs, Deadlines, and Escalation An item marked "done" can still leave the next person wondering what to do. A useful handoff tells them what is ready, what evidence to review, and who to contact if they get stuck. Use Tasks to make that next obligation visible, with its own owner and completion criteria. ### Decide What Done Means Use `Task.status` for its R4 lifecycle and `businessStatus` for local stages such as waiting for documents or awaiting review. Publish the local stage codes and permitted transitions. Use `statusReason` to explain the current blocked, failed, or cancelled state. For internally actionable work, `ready` can identify an item available to perform. Use `requested`, `received`, `accepted`, and `rejected` when the workflow explicitly exchanges and acknowledges a request for work. Set `in-progress` when execution begins, and a terminal status when it ends. Choose `intent` at creation; R4 Task intent is immutable. Before wiring up a Complete button, decide what the next person needs to see. A scheduling Task might require an Appointment reference; a response Task might require the resolving Communication; a clinical review Task might require a documented disposition. A recommendation may be declined even though its review Task was successfully completed. ```json { "resourceType": "Task", "id": "review-assessment", "status": "completed", "intent": "order", "code": { "text": "Review assessment" }, "for": { "reference": "Patient/example" }, "focus": { "reference": "QuestionnaireResponse/assessment" }, "owner": { "reference": "PractitionerRole/reviewer" }, "executionPeriod": { "start": "2026-10-06T16:00:00Z", "end": "2026-10-06T16:10:00Z" }, "output": [{ "type": { "text": "Follow-up request" }, "valueReference": { "reference": "ServiceRequest/follow-up" } }] } ``` The review is complete; the follow-up ServiceRequest still has its own fulfillment lifecycle. ### Create the Next Work Item When preparation completes, the application or a scoped transition Subscription can initiate review. Derive a stable identifier from the source work and next step, and conditionally create the review Task. Replaying the same transition should find that same Task. For changes that must succeed together, use a [transaction Bundle](/docs/fhir-datastore/fhir-batch-requests), version-check the update, and conditionally create the successor. Newly created resources that reference one another use `urn:uuid` fullUrls. A batch does not provide those transactional guarantees. :::caution[A parent link does not start the next Task] Use `partOf` for decomposition into subtasks. Store executable dependency rules in the application or Bot; neither a parent link nor the existence of a PlanDefinition releases work automatically. ::: ### Set Start Dates and Deadlines Use `authoredOn` for creation time, `executionPeriod` for actual work, and the [Task planned-date convention](/docs/careplans/tasks#task-start--due-dates) for start dates and deadlines. Use [range-aware date queries](/docs/careplans/tasks#searching-by-due-date-range) when a Task has both a start and an end. Compute business-hour deadlines in application or Bot logic using a defined time zone, calendar, and pause policy. Capture effective pause/resume times if waiting periods are excluded. Resource history records persistence times; use explicit transition records when those differ from the business event time. ### Give Stuck Work a Way Forward A scheduled Bot can find overdue open work and reassign it or create a distinct escalation Task. Preserve the original item and its history. Use a stable escalation identifier or milestone to prevent a new alert on every run. A reminder to a supervisor and a transfer to another coordinator are different actions. Decide whether escalation transfers ownership, requests help, or informs a supervisor. These have different completion rules. Failed automation must leave an actionable recovery path; see [Automating Care Workflows](/docs/careplans/automating-workflows) and [Operational Reporting](/docs/careplans/operational-reporting). --- ## Care Coordination A patient needs a follow-up visit. Someone has to review the assessment, contact the patient, arrange the visit, and make sure the result gets back to the care team. Each step is straightforward on its own. Keeping track of who owns the next step is where care coordination becomes useful. This section covers operational workflows across the practice, from a shared task queue to ongoing patient case management. Medplum provides the FHIR resources, search, access policies, and automation tools you can combine to build these workflows. :::tip[Start with the workflow] Use the [Care Coordination Decision Guide](/docs/decision-guides/care-coordination) to work through ownership, handoffs, and follow-up with your team. You can start with Tasks and add a case or clinical plan as your care model needs it. ::: ### Start with the Work Your Team Needs to Do | Need | Model | What it represents | | --- | --- | --- | | Assign and track an action | `Task` | A unit of work with its own owner, lifecycle, inputs, and outputs | | Request a clinical service | `ServiceRequest` or another specific request resource | The clinical request or authorization that work fulfills | | Coordinate ongoing participants | `CareTeam` | People and organizations involved in care, with roles and participation periods | | Track responsibility across visits | `EpisodeOfCare` | A period of responsibility for a patient, including organization, coordinator, and team | | Agree on a patient's care | `CarePlan` and `Goal` | Intended activities and objectives, with evidence of progress | | Reuse a clinical protocol | `PlanDefinition` and `ActivityDefinition` | Definitions that can be applied to patient-specific work | ```mermaid flowchart LR plan[CarePlan] -->|goal| goal[Goal] plan -->|activity.reference| task[Task] task -->|focus| request[ServiceRequest] plan -->|careTeam| team[CareTeam] plan -->|supportingInfo convention| episode[EpisodeOfCare] encounter[Encounter] -->|episodeOfCare| episode ``` This diagram shows one directly authored plan. Resources also carry their own patient references. The `supportingInfo` association is an application convention; a shared Condition alone does not establish case membership. Protocol-generated plans can have an intermediate RequestGroup. ### Build Your Care Coordination Workflow 1. [Tasks and Work Queues](/docs/careplans/tasks): identify work, expose queues, and claim items safely. 2. [Teams, Assignment, and Top-of-License Care](/docs/careplans/teams-and-delegation): assign eligible roles and preserve required review. 3. [Handoffs, Deadlines, and Escalation](/docs/careplans/handoffs-and-escalation): define completion evidence, dependencies, and exceptions. 4. [Longitudinal Patient Case Tracking](/docs/careplans/longitudinal-patient-case-tracking): maintain continuity and responsibility across visits. 5. [Care Plans, Goals, and Progress](/docs/careplans/care-plans-and-goals): connect planned work to clinical objectives. 6. [Automating Care Workflows](/docs/careplans/automating-workflows): create and advance work reliably. 7. [Clinical Decision Support and Follow-Through](/docs/careplans/clinical-decision-support): connect guidance to review and action. 8. [Operational Reporting](/docs/careplans/operational-reporting): measure workload, delays, and outcomes. ### Connect the Rest of the Practice [Referrals](/docs/careplans/referrals), [messaging](/docs/communications/messaging-data-model), and [scheduling](/docs/scheduling) use these coordination patterns. Keep domain-specific clinical records in those models and use Tasks for the work around them. [Clinical Protocols](/docs/careplans/protocols) covers reusable definition authoring. Enforce visibility through [AccessPolicy and ProjectMembership](/docs/access/access-policies). Assignment and CareTeam membership do not automatically grant access. Populate each resource's own patient references, including work created by Bots. For the operational motivation, see [Digital Health is an Operations Game](/blog/digital-health-operations), particularly its discussion of task classification, delegation, and asynchronous care. --- ## LOINC Codes [LOINC](https://loinc.org) (Logical Observation Identifiers Names and Codes) was established with the intention of defining a universal standard for identifying clinical data in electronic reports. The overall scope of LOINC is anything you can **test**, **measure**, or **observe** about a patient. This guide aims to provide a basic understanding of LOINC codes and how they are applied in practice. Adopting LOINC codes for clinical data helps: - Leverage clinical best practices for recording patient data - Streamline [CLIA/CAP](/docs/compliance/clia-cap) and [ONC certification](/docs/compliance/onc) - Simplify billing for diagnostics LOINC codes are most often used on [`Observation`](/docs/api/fhir/resources/observation) and [`ObservationDefinition`](/docs/api/fhir/resources/observationdefinition) resources to indicate the quantity being measured, and the US core guidelines require LOINC codes to be used for their [representation of patient vitals](/docs/fhir-datastore/understanding-uscdi-dataclasses). You can learn more about LOINC codes by reading their [getting started guide](https://loinc.org/get-started/), and you can search for relevant codes using the [LOINC online browser](https://loinc.org/search/). ### Types of LOINC Codes At the highest level, there are two major divisions of LOINC codes: **Laboratory** and **Clinical** #### Laboratory Laboratory codes represent anything that you can measure or observe about a specimen extracted from a patient. It contains the usual categories of chemistry, hematology, serology, microbiology, toxicology; as well as categories for cell counts, antibiotic susceptibilities, and more 1. ##### Examples | Name | LOINC Code | | ---------------------------------------- | ------------------------------------- | | Sodium [Moles/volume] in Blood | [55231-5](https://loinc.org/55231-5/) | | Leukocytes [#/volume] in Blood | [26464-8](https://loinc.org/26464-8/) | | Toxoplasma gondii Ab [Presence] in Serum | [22577-1](https://loinc.org/22577-1/) | #### Clinical Codes Clinical codes represent "everything else" excluding laboratory results - anything you can measure about a patient _without_ removing a specimen from them. This includes codes for vitals signs, hemodynamics, imaging, EKG, etc. as well as results from selected survey instruments (e.g PHQ-9 depression scale, CMS-required patient assessment instruments, etc.) 1. ##### Examples | Name | LOINC Code | | --------------------- | ------------------------------------- | | Heart Rate | [8867-4](https://loinc.org/8867-4/) | | Smoking status [FTND] | [63638-1](https://loinc.org/63638-1/) | #### :::tip[Deep Dive: LOINC code components] Each LOINC code description follows a regular schema with six components 2 | Component | Description | Example | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | | Component (Analyte) | The substance or entity being measured or observed. | Leukocytes (white blood cells) | | Property | The characteristic or attribute of the analyte. | NCnc (Number concentration) | | Time | The interval of time over which an observation was made. | Pt (Point in time) | | System (Specimen) | The specimen or thing upon which the observation was made. | CSF (Cerebral spinal fluid) | | Scale | How the observation value is quantified or expressed: quantitative, ordinal, nominal. | Qn (Quantitative) | | Method | (Optional) A high-level classification of how the observation was made. Only needed when the technique affects the clinical interpretation of the results. | Manual Count | These six parts care then translated into "LOINC names" to represent each concept. There are 3 different levels of specificity for LOINC names: | Level | **Description** | Example | | -------------------------- | -------------------------------------------- | -------------------------------------------------------------- | | Fully-Specified Name (FSN) | Formal name consisting of all six code parts | Leukocytes: NCnc: Pt: CSF: Qn: Manual count | | Long Common Name (LCN) | Clinician-friendly display name | Leukocytes [#/volume] in Cerebral spinal fluid by Manual count | | Short Name | Short code for column headers & reporting | WBC # CSF Manual | You can find more information about LOINC code parts [here](https://loinc.org/kb/faq/structure/) ::: ### Collection Codes In addition to individual clinical findings, LOINC includes codes for common collections of findings, also known as "panels," "batteries," "surveys," or "questionnaires" depending on the clinical context. ##### Examples | **Panel Name** | Type | LOINC Code | Members | | ------------------------------------------- | ------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Patient Health Questionnaire 4 item (PHQ-4) | Questionnaire | [69725-0](https://loinc.org/69724-3/) | [69725-0](https://loinc.org/69725-0) - Feeling nervous, anxious or on edge [68509-9](https://loinc.org/68509-9) - Over the past 2 weeks have you not been able to stop or control worrying [44250-9](https://loinc.org/44250-9) - Little interest or pleasure in doing things [44255-8](https://loinc.org/44255-8) - Feeling down, depressed, or hopeless [70272-0](https://loinc.org/70272-0) - Patient Health Questionnaire 4 item (PHQ-4) total score [Reported] | | Basic metabolic panel - Blood | Lab Panel | [51990-0](https://loinc.org/51990-0/) | [2339-0](https://loinc.org/2339-0) -Glucose [Mass/volume] in Blood [6299-2](https://loinc.org/6299-2) - Urea nitrogen [Mass/volume] in Blood [38483-4](https://loinc.org/38483-4) - Creatinine [Mass/volume] in Blood [44734-2](https://loinc.org/21550) - Urea nitrogen/Creatinine [Mass Ratio] in Blood [49765-1](https://loinc.org/44734-2) - Calcium [Mass/volume] in Blood [55231-5](https://loinc.org/55231-5) - Electrolytes panel - Blood | You can find the constituent members of a collection by browsing [loinc.org](https://loinc.org) page for a collection's LOINC code and navigating "panel hierarchy" section. ### Questionnaire Answers and Answer Lists The ambition of LOINC is to standardize clinical data collection across organizations and clinical settings. To standardize responses to questionnaires, LOINC provides standard codes not only for questions, _but also_ to answer choices for multiple-choice questions. These LOINC codes are prefixed with `LA`, which stands for "LOINC Answer". These answer choices are then bundled into 'answer lists', which are annotated with their own distinctive LOINC codes, prefixed with `LL`. What's fantastic about the LOINC system is its intuitiveness. For instance, when you're browsing loinc.org and you view the code for a question, you're also shown any recommended answer values. This level of interconnectedness makes navigating and understanding the LOINC system that much more user-friendly. ##### Examples | Question | Answer List | Answers | | -------------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Feeling nervous, anxious or on edge ([69725-0](https://loinc.org/69725-0)) | [LL358-3](https://loinc.org/LL358-3) | Not at all ([LA6568-5](https://loinc.org/LA6568-5)) Several days ([LA6569-3](https://loinc.org/LA6569-3)) More than half the days ([LA6570-1](https://loinc.org/LA6570-1)) Nearly every day ([LA6571-9](https://loinc.org/LA6571-9)) | | Age group ([46251-5](https://loinc.org/46251-5)) | [LL2435-7](https://loinc.org/LL2435-7) | Infant ([LA19747-7](https://loinc.org/LA19747-7)) Newborn ([LA10403-6](https://loinc.org/LA10403-6)) Child ([LA9949-4](https://loinc.org/LA9949-4)) Pre-school ([LA19748-5](https://loinc.org/LA19748-5)) Adolescent ([LA19749-3](https://loinc.org/LA19749-3)) Adult ([LA13524-6](https://loinc.org/LA13524-6)) Middle aged ([LA19750-1](https://loinc.org/LA19750-1)) Aged ([LA19751-9](https://loinc.org/LA19751-9)) |
Example: PHQ-4 Questionnaire ```json { "resourceType": "Questionnaire", "title": "Patient Health Questionnaire 4 item (PHQ-4) [Reported]", "status": "draft", "code": [ { "code": "69724-3", "display": "Patient Health Questionnaire 4 item (PHQ-4) [Reported]", "system": "http://loinc.org" } ], "meta": { "profile": ["http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire|2.7"] }, "item": [ { "type": "choice", "code": [ { "code": "69725-0", "display": "Feeling nervous, anxious or on edge" } ], "required": false, "linkId": "/69725-0", "text": "Feeling nervous, anxious or on edge", "answerOption": [ { "valueCoding": { // highlight-start "code": "LA6568-5", "display": "Not at all", "system": "http://loinc.org" // highlight-end } }, { "valueCoding": { // highlight-start "code": "LA6569-3", "display": "Several days", "system": "http://loinc.org" // highlight-end } }, { "valueCoding": { // highlight-start "code": "LA6570-1", "display": "More than half the days", "system": "http://loinc.org" // highlight-end } }, { "valueCoding": { // highlight-start "code": "LA6571-9", "display": "Nearly every day", "system": "http://loinc.org" // highlight-end } } ] }, { "type": "choice", "code": [ { "code": "68509-9", "display": "Over the past 2 weeks have you not been able to stop or control worrying", "system": "http://loinc.org" } ], "required": false, "linkId": "/68509-9", "text": "Over the past 2 weeks have you not been able to stop or control worrying", "answerOption": [ { "valueCoding": { // highlight-start "code": "LA6568-5", "display": "Not at all", "system": "http://loinc.org" // highlight-end } }, { "valueCoding": { // highlight-start "code": "LA6569-3", "display": "Several days", "system": "http://loinc.org" // highlight-end } }, { "valueCoding": { // highlight-start "code": "LA18938-3", "display": "More days than not", "system": "http://loinc.org" // highlight-end } }, { "valueCoding": { // highlight-start "code": "LA6571-9", "display": "Nearly every day", "system": "http://loinc.org" // highlight-end } } ] }, { "type": "choice", "code": [ { "code": "44250-9", "display": "Little interest or pleasure in doing things", "system": "http://loinc.org" } ], "required": false, "linkId": "/44250-9", "text": "Little interest or pleasure in doing things", "answerOption": [ { "valueCoding": { // highlight-start "code": "LA6568-5", "display": "Not at all", "system": "http://loinc.org" // highlight-end } }, { "valueCoding": { // highlight-start "code": "LA6569-3", "display": "Several days", "system": "http://loinc.org" // highlight-end } }, { "valueCoding": { // highlight-start "code": "LA6570-1", "display": "More than half the days", "system": "http://loinc.org" // highlight-end } }, { "valueCoding": { // highlight-start "code": "LA6571-9", "display": "Nearly every day", "system": "http://loinc.org" // highlight-end } } ] }, { "type": "choice", "code": [ { "code": "44255-8", "display": "Feeling down, depressed, or hopeless", "system": "http://loinc.org" } ], "required": false, "linkId": "/44255-8", "text": "Feeling down, depressed, or hopeless", "answerOption": [ { "valueCoding": { // highlight-start "code": "LA6568-5", "display": "Not at all", "system": "http://loinc.org" // highlight-end } }, { "valueCoding": { // highlight-start "code": "LA6569-3", "display": "Several days", "system": "http://loinc.org" // highlight-end } }, { "valueCoding": { // highlight-start "code": "LA6570-1", "display": "More than half the days", "system": "http://loinc.org" // highlight-end } }, { "valueCoding": { // highlight-start "code": "LA6571-9", "display": "Nearly every day", "system": "http://loinc.org" // highlight-end } } ] }, { "type": "decimal", "code": [ { "code": "70272-0", "display": "Patient health questionnaire 4 item total score", "system": "http://loinc.org" } ], "required": false, "linkId": "/70272-0", "text": "Patient health questionnaire 4 item total score", "item": [ { "text": "The PHQ-4 is different -- although the total score (which ranges from 0-12) can be used, it is really a combination of the PHQ-2 depression scale (described above) and the GAD-2 anxiety scale (from the parent GAD-7 anxiety scale). Thus, another way to look at it is a 0-6 depression subscale and a 0-6 anxiety scale. It is clearly different than either the PHQ-9 or the PHQ-2", "type": "display", "linkId": "/70272-0-help" } ] } ] } ```
### See Also - [[1] Scope of Loinc](https://loinc.org/get-started/scope-of-loinc) - [[2] LOINC Term Basics](https://loinc.org/get-started/loinc-term-basics) - [LOINC Typeahead](https://clinicaltables.nlm.nih.gov/apidoc/loinc/v3/doc.html) and other [code system typeaheads](https://clinicaltables.nlm.nih.gov/) can be found on the NIH website - [LOINC Browser](https://loinc.org/search) - [LOINC Top 2000 Lab Observations](https://loinc.org/usage/obs/) - [Social Determinants of Health (SDOH) LOINC Groups](https://loinc.org/search/?t=4&s=SDH) --- ## Longitudinal Patient Case Tracking import ExampleCode from '!!raw-loader!@site/../examples/src/careplans/longitudinal-tracking-examples.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; A patient may have several visits, messages, and follow-up Tasks during one care program. The team still needs a way to answer: who is responsible for this case, and is it still active? Use an [EpisodeOfCare](/docs/api/fhir/resources/episodeofcare) for that period of responsibility. A program enrollment, treatment course, or ongoing case can justify an episode. Independent tasks or a single visit can use their existing resources. ### Start a Case and Name the Responsible Team | Field | Purpose | | --- | --- | | `patient` | Patient whose care is being tracked | | `identifier` | Stable business identity for the enrollment or case | | `status` and `statusHistory` | Current lifecycle and periods spent in earlier states | | `type` | Program or kind of episode | | `period` | Period of responsibility | | `managingOrganization` | Organization responsible for the episode | | `careManager` and `team` | Responsible coordinator and CareTeam references | | `diagnosis.condition` | Conditions relevant to this episode | EpisodeOfCare can record both coordinator and team assignments. Clinical goals and planned activities belong in CarePlan and Goal. :::tip[Two cases can share a diagnosis] Create each case with its own identity. Two concurrent episodes can address the same Condition; the diagnosis is not the case identifier. Use conditional create on a stable enrollment identifier when imports or launch requests may repeat. ::: ### Bring Visits and Plans into the Case ```mermaid flowchart LR visit1[Encounter 1] -->|episodeOfCare| episode[EpisodeOfCare] visit2[Encounter 2] -->|episodeOfCare| episode plan[CarePlan] -->|supportingInfo convention| episode plan -->|addresses| condition[Condition] episode -->|diagnosis.condition| condition plan -->|goal| goal[Goal] episode -->|team| team[CareTeam] ``` `Encounter.episodeOfCare` is an array, so one visit can contribute to multiple episodes. Use Appointment for the booking workflow; create a planned Encounter only when the encounter model calls for it, rather than creating one merely to group an appointment. :::note[Choose the plan-to-case convention explicitly] For a plan formed in the context of a case, this guide uses `CarePlan.supportingInfo` to reference the EpisodeOfCare. This is a supporting-context convention, not a dedicated R4 case-membership field. Decide whether one plan can relate to multiple episodes and document that rule consistently. ::: ```json { "resourceType": "CarePlan", "status": "active", "intent": "plan", "title": "Patient follow-up plan", "subject": { "reference": "Patient/example" }, "supportingInfo": [{ "reference": "EpisodeOfCare/program-enrollment" }], "careTeam": [{ "reference": "CareTeam/patient-team" }], "activity": [{ "reference": { "reference": "Task/follow-up" } }] } ``` Use a stable enrollment key to create the episode and its plan. This function takes references to an existing patient and organization and uses the episode's returned ID in the plan. Each conditional create can be retried with the same key if the process stops between writes; existing records retain their current state. {ExampleCode} Apply the [patient-context and access rules](/docs/careplans/teams-and-delegation#assignment-and-access) to the linked records. ### Find the Records for One Case ```http GET /fhir/R4/EpisodeOfCare?patient=Patient/example&status=active GET /fhir/R4/Encounter?episode-of-care=EpisodeOfCare/program-enrollment GET /fhir/R4/CarePlan?patient=Patient/example ``` For the last query, follow pagination and select plans whose `supportingInfo` explicitly references the episode. This selection resolves an association among already authorized resources; access remains enforced by AccessPolicy. This example follows every page of authorized plans and checks the explicit episode reference: {ExampleCode} A Condition search can find plans addressing a diagnosis, but cannot uniquely find an episode's plans. There is no standard R4 CarePlan search parameter for this `supportingInfo` association. If direct server-side episode-to-plan queries are essential at scale, agree on and validate a custom SearchParameter before relying on one. For work authorized by the plan, `Task.basedOn` can reference the CarePlan; keep `focus` for the actual request or resource being acted on. Work directly concerning case administration can focus on the EpisodeOfCare. Avoid overwriting a clinical focus merely to simplify case grouping. Other case-level work may need a documented grouping convention; an Encounter reference alone cannot identify one case when the visit spans several. ### Transfer or Close the Case Without Dropping Work For a coordinator change within the same period of organizational responsibility, update `careManager` or team membership and retain history. Update individual open Task owners separately. A transfer to another organization can require a new episode, particularly when responsibility periods or system ownership differ; agree on the transfer boundary instead of silently reassigning the entire history. Before setting the episode to `finished`, review open plans and Tasks. Complete, cancel, or transfer each obligation according to its own criteria. Set the episode's end date and retain the evidence of who remained responsible at each stage. Finishing an episode does not automatically close its plans, goals, appointments, or Tasks. See [Care Plans and Goals](/docs/careplans/care-plans-and-goals) and [Teams and Delegation](/docs/careplans/teams-and-delegation). The [Chamber Cardio case study](/blog/chamber-cardio-case-study) provides an example of the broader care-coordination problem these models support. --- ## Operational Reporting A coordinator needs to see what is waiting today. A team lead may want to know why the same queue took longer to clear last month. These views use the same workflow records, but they need different queries. Use current FHIR resources for actionable lists. For historical measures, capture the transitions that tell you when work arrived, changed hands, paused, and finished. ### Show the Team What Needs Attention Today Use Task searches for owner, lifecycle status, work type, and local business stage. For example: ```http GET /fhir/R4/Task?owner=PractitionerRole/coordinator&status=ready,in-progress,on-hold GET /fhir/R4/Task?owner:missing=true&status=ready GET /fhir/R4/Task?focus=ServiceRequest/example ``` An unowned pool uses `owner:missing=true`; a group-owned queue searches for that Organization, HealthcareService, or CareTeam instead. Add the work-type or role filter that defines the pool. Follow pagination rather than treating the first page as the complete queue. For deadlines, follow the [Task date-range guidance](/docs/careplans/tasks#searching-by-due-date-range). Use the [planned-date convention](/docs/careplans/handoffs-and-escalation#represent-time-accurately) consistently, including its interoperability limits, and account for the project's date-range indexing behavior. Tasks with no deadline are a separate group, not automatically on time. Run operational and aggregate queries under the [access rules agreed for the workflow](/docs/careplans/teams-and-delegation#assignment-and-access). ### Capture When Work Changed Hands :::caution[Editing a note should not reset queue age] `Task.lastModified` and `meta.lastUpdated` describe the latest edit, which might be a note correction. Neither field alone tells you when the current stage began. Compare [resource-history versions](/docs/fhir-datastore/resource-history) to detect stage and owner changes. ::: For structured actor, reason, or business-event time, create `Provenance` targeting the affected resource version. Use `occurredDateTime` for the effective event time and `recorded` for when it was recorded. These can differ for delayed inbound updates. Provenance supplements history; it does not automatically contain old and new status values. A reporting table can contain: | Column | Meaning | | --- | --- | | Task ID and resource version | Stable identity and source-version deduplication key | | Previous/new lifecycle, stage, and owner | What changed, derived from the relevant versions | | Effective time and recorded time | When it happened and when the system learned about it | | Actor and reason | Context from the workflow record or explicit Provenance | | Patient, request, and plan references | Dimensions for authorized aggregation | Build this table in your reporting store. Make ingestion replayable, tolerate late events, and retain enough source history to rebuild intervals. ### Agree on What the Clock Measures Two teams can both report "turnaround time" and measure different things. Settle the start, stop, and pause rules before comparing their numbers. | Measure | Definition to settle | | --- | --- | | Queue age | Time since initial readiness, arrival at the current team, or entry into the current stage | | Turnaround time | Start/end events, business calendar, and excluded waiting intervals | | Overdue work | Which open states count and what happens when a clock pauses | | Handoff rate | Owner changes versus distinct independently assigned steps | | Completion rate | Eligible population, time window, and treatment of cancellations or reopened items | Count work at the appropriate level. One referral may have several Tasks, so completed Task counts do not produce a referral completion rate. A reopened item needs another interval; do not replace its original completion time in reporting history. ### Put Work Completion Alongside Patient Progress Use Goal targets, `achievementStatus`, and supporting Observations for clinical progress. Report them alongside work completion without treating one as proof of the other. The clinical team defines measures and denominators; the pipeline preserves the evidence and timing needed to calculate them. :::tip[Start with one measure you can explain] Start with an open-work dashboard and one well-defined turnaround measure. Add measures once transitions and exception handling are reliably captured. ::: --- ## Clinical Protocols In FHIR, care workflows and clinical protocols are primarily modeled using the **`PlanDefinition`** resource. While a `CarePlan` represents a concrete plan for a *specific patient*, a `PlanDefinition` represents the abstract protocol that can be applied to *any patient*. At its core, a `PlanDefinition` contains a list of **`action`** elements, where each action represents a specific step in the workflow. The `action` element is extremely expressive and allows for incredibly rich workflow logic. ### Basic Sequential Tasks At its most basic level, a `PlanDefinition` is just a simple list of sequential tasks. You can define the task details directly inline: ```json { "resourceType": "PlanDefinition", "status": "active", "title": "Basic Onboarding Protocol", "action": [ { "title": "Schedule Initial Consult", "description": "Call patient to schedule their first appointment." }, { "title": "Send Welcome Packet", "description": "Email the welcome packet and intake forms." } ] } ``` ### Reusable Definitions (ActivityDefinition & Questionnaire) Instead of defining all task details inline, actions often reference standalone, reusable definition resources using the `definitionCanonical` element. This allows you to maintain a single source of truth for common clinical activities across multiple protocols. #### ActivityDefinition An [`ActivityDefinition`](/docs/api/fhir/resources/activitydefinition) is a template for a specific clinical act, such as ordering a lab test, prescribing a medication, or performing a procedure. When an action references an `ActivityDefinition`, the protocol inherits its properties. Furthermore, the `ActivityDefinition.kind` element tells the system what type of resource to generate when the protocol is executed: - If `kind` is set to `ServiceRequest`, executing the protocol will generate a draft `ServiceRequest` resource (copying over the `code`, `intent`, etc.) alongside the `Task`, and link them together. - If `kind` is set to `Task` (or left blank), it will simply generate a `Task`. ```js // The PlanDefinition represents the overall protocol { "resourceType": "PlanDefinition", "status": "active", "action": [ { "title": "Order HbA1c Test", // This canonical URL links the action to the ActivityDefinition below "definitionCanonical": "http://example.org/ActivityDefinition/hba1c-order" } ] } // The ActivityDefinition is a reusable template for the specific lab order { "resourceType": "ActivityDefinition", "url": "http://example.org/ActivityDefinition/hba1c-order", "status": "active", "kind": "ServiceRequest", "code": { "coding": [ { "system": "http://loinc.org", "code": "4548-4", "display": "HbA1c" } ] } } ``` #### Questionnaire If an action requires collecting structured data from a patient or provider, it can reference a [`Questionnaire`](/docs/api/fhir/resources/questionnaire). When executed, the system generates a `Task` whose `input` references the Questionnaire, signaling to your application that this specific form needs to be completed to fulfill the task. ```json { "resourceType": "PlanDefinition", "status": "active", "action": [ { "title": "Complete Risk Assessment", "definitionCanonical": "http://example.org/Questionnaire/diabetes-risk" } ] } ``` ### Advanced Workflow Logic The FHIR standard supports highly complex workflow orchestration. #### Hierarchical Actions & Grouping You can nest actions to create phases or groups, and define rules for how users select from them using `groupingBehavior` (e.g., `logical-group`, `visual-group`) and `selectionBehavior` (e.g., `any`, `all`, `exactly-one`). ```json { "resourceType": "PlanDefinition", "status": "active", "action": [{ "title": "Initial Lab Orders", "groupingBehavior": "logical-group", "selectionBehavior": "any", "action": [ { "title": "Order CBC" }, { "title": "Order CMP" } ] }] } ``` #### Conditional Actions You can define if/else logic or prerequisites using expressions (like CQL or FHIRPath) to determine if a step should occur. ```json { "resourceType": "PlanDefinition", "status": "active", "action": [{ "title": "Administer Medication", "condition": [{ "kind": "applicability", "expression": { "language": "text/fhirpath", "expression": "%patient.birthDate <= today() - 18 years" } }] }] } ``` #### Action Timing & Dependencies You can trigger actions based on the completion of other actions, including time delays. ```json { "resourceType": "PlanDefinition", "status": "active", "action": [ { "id": "step1", "title": "Initial Assessment" }, { "title": "Follow-up Assessment", "relatedAction": [{ "actionId": "step1", "relationship": "after-end", "offsetDuration": { "value": 7, "unit": "days", "system": "http://unitsofmeasure.org", "code": "d" } }] } ] } ``` #### Dynamic Values You can dynamically inject values into the resulting resources based on the context (like the patient's name) when the protocol is applied. ```json { "resourceType": "PlanDefinition", "status": "active", "action": [{ "title": "Create Follow-up Task", "dynamicValue": [{ "path": "Task.description", "expression": { "language": "text/fhirpath", "expression": "'Follow up with ' + %patient.name.first().given.first()" } }] }] } ``` ### Executing Protocols To actually start a workflow for a specific patient, you instantiate the `PlanDefinition` using the [`$apply` operation](/docs/api/fhir/operations/plandefinition-apply). When you invoke the `$apply` operation, Medplum processes the PlanDefinition and: 1. **Creates Tasks** for each action in the PlanDefinition 2. **Resolves ActivityDefinitions & Questionnaires** to generate the appropriate underlying resources (like `ServiceRequest`). 3. **Creates a RequestGroup** containing all the generated actions. 4. **Creates a CarePlan** that wraps the RequestGroup. :::info[Note on Medplum Support] The FHIR standard allows for very complex workflow modeling, including hierarchical actions, conditional logic, and dynamic values. While you can author and store these complex `PlanDefinition` resources in Medplum today, the Medplum [`$apply` operation](/docs/api/fhir/operations/plandefinition-apply) currently implements a focused subset of these capabilities (primarily generating sequential Tasks and resolving ActivityDefinitions). Full execution support for hierarchical and conditional logic is under active development. ::: #### Action Definition Processing | Definition Type | Generated Resources | | -------------------- | ------------------------------------------------- | | `Questionnaire` | Task with Questionnaire input reference | | `ActivityDefinition` | Task + ServiceRequest (if kind is ServiceRequest) | | None | Task only | #### Task Extensions Medplum supports custom extensions on `ActivityDefinition` resources to dynamically assign Task parameters when `$apply` is run. - `https://medplum.com/fhir/StructureDefinition/task-elements` - Configure Task `owner` and `performerType` using FHIRPath expressions based on the `$apply` parameters (e.g., `%patient`, `%practitioner`, `%organization`). **Example ActivityDefinition with Task Extensions:** ```json { "resourceType": "ActivityDefinition", "status": "active", "extension": [{ "url": "https://medplum.com/fhir/StructureDefinition/task-elements", "extension": [ { "url": "owner", "valueExpression": { "language": "text/fhirpath", "expression": "%practitioner" } }, { "url": "performerType", "valueCodeableConcept": { "coding": [{ "system": "http://snomed.info/sct", "code": "158965000", "display": "Medical practitioner" }] } } ] }] } ``` When the protocol is applied, Medplum evaluates these expressions to generate a concrete `Task` resource with the resolved values. ```js {6,7,12-13} { "resourceType": "Task", "status": "requested", "intent": "order", "for": { "reference": "Patient/example" }, // The owner is resolved from the %practitioner FHIRPath expression "owner": { "reference": "Practitioner/dr-alice-smith" }, // The performerType is resolved from the valueCodeableConcept in the extension "performerType": [ { "coding": [ // The coding is resolved from the extension's valueCodeableConcept { "system": "http://snomed.info/sct", "code": "158965000", "display": "Medical practitioner" } ] } ] } ``` --- ## Observation Reference Ranges import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; import ExampleCode from '!!raw-loader!@site/..//examples/src/careplans/reference-ranges.ts'; In on our previous guide about [creating diagnostic services catalog](./diagnostic-catalog), we described the importance of the [`ObservationDefinition`](/docs/api/fhir/resources/observationdefinition) resource for storing metadata about the [`Observations`](/docs/api/fhir/resources/observation) produced by the test. This metadata is not just for ensuring data correctness, but also a key component in assisting providers with data interpretation. A core part of interpreting results of this metadata is the definition of **reference ranges**, sets of [`Observation`](/docs/api/fhir/resources/observation) values that share a **diagnostic interpretation**. While medical directors will always have the final say on any interpretation, incorporating reference ranges into your catalog can streamline their workflow. In this guide, we'll take a closer look at how administrators can use the [`ObservationDefinition`](/docs/api/fhir/resources/observationdefinition) resource to define these ranges of interest. We'll cover the following key areas: 1. Defining a reference range. 2. Establishing age and gender-dependent ranges. 3. Understanding the types of ranges that FHIR supports. 4. Editing reference ranges in the medplum UI. 5. Defining ranges for non-numerical Observations. ### Defining a Reference Range A reference range is defined using the `ObservationDefinition.qualifiedInterval` element. This is an array field, which allows administrators to define multiple ranges of interest for a single [`Observation`](/docs/api/fhir/resources/observation). A range is defined by the following properties: | **Element** | Description | Example | | ----------------------------------- | ------------------------------------------------------------------------------------------ | --------- | | `qualifiedInterval.range.low` | Lower bound for the reference range, inclusive. (Empty value denotes no lower bound) | 10 mg/dL | | `qualifiedInterval.range.high` | Upper bound for the reference range, inclusive. (Empty value denotes no upper bound) | 100 mg/dL | | `qualifiedInterval.range.condition` | How to interpret observation values in this interval | "High" |
Example: 10 - 100 mg/dL {ExampleCode}
Example: Less or equal to than 5 mg/dL {ExampleCode}
Example: Greater than or equal to 20 mg/dL {ExampleCode}
In practice, multiple an [`ObservationDefinition`](/docs/api/fhir/resources/observationdefinition) will define multiple reference ranges for a given patient population, to provide interpretations for each value of the [`Observation`](/docs/api/fhir/resources/observation).
Example: Multiple Ranges The example below defines three reference ranges, to be interpreted as "Low", "Normal", and "High". {ExampleCode}
### Patient-dependent reference ranges Patient demographics can influence the interpretation of some diagnostic test results. To handle this, the [`ObservationDefinition`](/docs/api/fhir/resources/observationdefinition) resource allows for defining reference ranges that can be either tailored to specific patient demographics or universally applied. The table below describes which patient attributes can be used to target reference ranges: | **Attribute** | Element | **Type** | Code System | Example | | ------------------- | ---------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------- | ------- | | Age | `qualifiedInterval.age` | Range | | | | Gender | `qualifiedInterval.gender` | code | [AdministrativeGender](https://hl7.org/fhir/R4/valueset-administrative-gender.html) | female | | Gestational Age | `qualifiedInterval.gestationalAge` | Range | | | | Racial/Ethnic Group | `qualifiedInterval.appliesTo` | CodeableConcept | _(Example)_ [OMB Race Categories](https://build.fhir.org/ig/HL7/US-Core/ValueSet-omb-race-category.html) | Asian |
Example: Age and gender dependent reference ranges The example below demonstrates how to represent different normal ranges for a testosterone test, for both adults and children. {ExampleCode}
:::note[] For any range, if these demographic qualifiers are left unspecified, the system interprets it as a universal range applicable to all patients. ::: :::tip[Matching [`Patients`](/docs/api/fhir/resources/patient) to reference ranges] The Medplum SDK provides helper functions,[ `findObservationInterval`](/docs/sdk/core.findobservationinterval), [`findObservationReferenceRange`](/docs/sdk/core.findobservationreferencerange), and [`matchesRange`](/docs/sdk/core.matchesrange) to find reference ranges that match a particular patient.
Example {' '} {ExampleCode}
::: ### Types of reference ranges Reference ranges can be categorized into three different types: | Field | Description | | :---------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `reference` | Defines the **normal** ranges for a given observation type. | | `critical` | Defines the **critical** values for the observation, also known as "panic" values. Observing values in these ranges often require special handling to notify patients. | | `absolute` | The absolute allowable range for this value (i.e. the measurable range). Values outside of this range are not possible / sensible. | The type of reference range is specified in the `qualifiedInterval.category` field.
Example {ExampleCode}
### Editing reference ranges in the Medplum App Medplum has developed user interface tools to simplify the process of editing [`ObservationDefinition`](/docs/api/fhir/resources/observationdefinition) reference ranges. The [ReferenceRangeEditor](https://storybook.medplum.com/?path=/story/medplum-referencerangeeditor--empty) component enables you to define groups of reference ranges for each set of patient attributes. You can integrate this component into your custom application, or you can use it directly within the Medplum App. To edit reference ranges in the Medplum App: 1. Navigate to the Medplum App's ObservationDefinition page at https://app.medplum.com/ObservationDefinition. 2. Select the specific [`ObservationDefinition`](/docs/api/fhir/resources/observationdefinition) resource you wish to edit. 3. Click on the "Ranges" tab. #### Examples - [HDL Reference Ranges](https://storybook.medplum.com/?path=/story/medplum-referencerangeeditor--hdl) - no dependence on patient attributes. - [Testosterone Reference Ranges](https://storybook.medplum.com/?path=/story/medplum-referencerangeeditor--testosterone) - dependent on patient gender and age. ### Non-numeric reference ranges For qualitative observations, defining interpretations in the [`ObservationDefinition`](/docs/api/fhir/resources/observationdefinition) is slightly different. Rather than defining numerical ranges, the administrator defines [`ValueSet`](/docs/api/fhir/resources/valueset) resources that enumerate sets of codes. The `validCodedValueSet`, `normalCodedValueSet`, `abnormalCodedValueSet`, and `criticalCodedValueSet` fields mirror the functionality of `absolute`, `reference`, and `critical` reference ranges, are used to define the interpretation of a qualitative observation. Each one of these fields refers to a [`ValueSet`](/docs/api/fhir/resources/valueset) resource, which enumerates the codes assigned to each category of interpretation: | Field Name | Similar to | Description | | ----------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `validCodedValueSet` | `absolute` | Enumerates of _all_ valid qualitative values for this Observation type. | | `normalCodedValueSet` | `reference` | Enumerates all codes that signify a _normal_ result for this Observation. It should be a subset of the `validCodedValueSet`. | | `abnormalCodedValueSet` | `reference` | Enumerates all of codes that signify an _abnormal_ result for this Observation. It should be a subset of the `validCodedValueSet`. | | `criticalCodedValueSet` | `critical` | Enumerates all the qualitative values that are considered _critical_ or cause for "panic". Typically a subset of the `abnormalCodedValueSet`. | ### See Also - [LOINC Guide](./loinc) - [Diagnostic Catalog](./diagnostic-catalog) --- ## Creating and Capturing Referrals Referral forms vary by specialty, but the receiving team usually needs the same starting point: who the patient is, what service is requested, why it is needed, and who is requesting it. Capture that common information consistently, then add the specialty-specific details. ### Give Each Requested Service Its Own Identity Use one [ServiceRequest](/docs/api/fhir/resources/servicerequest) for each independently managed requested service. If a form requests several distinct services, create separate requests and use a shared `requisition` identifier when they were authorized together. | Field | What to capture | | --- | --- | | `subject` | The confirmed Patient reference | | `identifier` | A stable referral number in the assigning system's namespace | | `status` and `intent` | The request's lifecycle and whether it is a proposal, plan, or order | | `category` and `code` | Referral classification and the service requested | | `priority` | Clinically agreed urgency using R4 request-priority values | | `requester` | The person or organization responsible for the request | | `performer` | The intended clinician, organization, or service, when known | | `reasonCode` or `reasonReference` | The reason for the service, in text/coding or linked clinical records | | `supportingInfo` | Relevant assessments, results, or documents | | `encounter` | The originating encounter, when the request arose during one | :::tip[A referral does not need a visit first] You can create a referral outside a visit. Keep `encounter` optional for intake or outreach workflows rather than creating an Encounter just to fill the field. ::: ### Turn Form Answers into a Referral A simple application form can construct the ServiceRequest directly. For reusable forms, use a [Questionnaire](/docs/questionnaires) and retain the QuestionnaireResponse. A shared set of core questions helps specialty forms produce a consistent request model. Choose one [extraction strategy](/docs/questionnaires/parsing-questionnaire-responses) for each form: SDC template extraction or a Bot that maps the answers. Validate required answers, patient identity, allowed values, and the selected recipient before creating the request. Use the core QuestionnaireResponse helpers for nested and repeated answers rather than assuming every answer is a top-level item. ```mermaid flowchart LR form[QuestionnaireResponse] --> mapping[Extraction or Bot] mapping --> referral[ServiceRequest] task[Task] -->|focus| referral referral -->|supportingInfo| form ``` Keep an audit of the mapping and clinical author. Record the clinical author in `ServiceRequest.requester` and preserve the Bot's execution identity separately. ### Let the Clinician Review Before Release A draft order uses `status: draft` and `intent: order`. Once the authorized user releases it, change the status to `active` and record when it became actionable in `authoredOn`. Sending is a later operational step and can fail while the clinical order remains active. :::note[Draft and proposal mean different things] A proposal recommends care; a draft order is an order being prepared. If your workflow starts with a true proposal, retain it and create the authorized order with `basedOn` referencing the proposal. Do not use `intent` as a save-draft flag. See the [R4 ServiceRequest definitions](https://hl7.org/fhir/R4/servicerequest-definitions.html#ServiceRequest.intent). ::: This example shows a released order. The text labels are illustrative; configure clinically validated terminology for production forms. ```json { "resourceType": "ServiceRequest", "identifier": [{ "system": "https://example.org/referrals", "value": "REF-2026-001" }], "status": "active", "intent": "order", "priority": "routine", "code": { "text": "Specialist consultation" }, "subject": { "reference": "Patient/example" }, "requester": { "reference": "PractitionerRole/referring-clinician" }, "authoredOn": "2026-10-06T16:00:00Z", "reasonCode": [{ "text": "Clinical reason documented by the referring clinician" }] } ``` ### Make Submission Safe to Repeat A user may click Submit twice, or a Bot may receive the same event again. Use a stable submission or referral identifier for conditional create, including when a Bot processes the form. `createResourceIfNoneExist` avoids a search-then-create race. Replaying the same submission should retrieve the existing request. If the request and its initial Task must be created together, use a transaction Bundle with `urn:uuid` fullUrls for their cross-references. See the [Worked Referral Example](/docs/careplans/referrals/fhir-resource-examples). Conditional create does not apply later edits to an existing match; use an authorized, version-checked update path for amendments. Next, choose the [recipient and referral package](/docs/careplans/referrals/recipients-and-packages). --- ## Worked Referral Example Let's follow a referral from release to review of the returned note. A clinician requests a consultation, a coordinator sends the package, and the receiving team schedules the patient. After the visit, the referring team reviews the specialist's note. Both teams use one Medplum project in this example. The package is shared through an in-app conversation, and each team works with the same ServiceRequest. For an external recipient, the integration exchanges these records and maps identifiers as described in [Referrals](/docs/careplans/referrals#decide-whether-the-teams-share-a-project). The examples use fictional IDs and text labels. Configure validated terminology and local workflow CodeSystems, and use your server's returned IDs for later references. ### 1. Create the Request and Initial Work Assume `Patient/example`, `PractitionerRole/referring-clinician`, `Organization/referral-coordination`, and `Organization/receiving-clinic` already exist. The transaction creates an active order and a Task to coordinate it. The Task's `focus` uses the request's `urn:uuid` fullUrl so the server can resolve the new reference. ```json { "resourceType": "Bundle", "type": "transaction", "entry": [ { "fullUrl": "urn:uuid:2bcb56d0-993c-4e86-9ae2-3ca0c5dfa093", "resource": { "resourceType": "ServiceRequest", "identifier": [{ "system": "https://example.org/referrals", "value": "REF-2026-001" }], "status": "active", "intent": "order", "code": { "text": "Specialist consultation" }, "subject": { "reference": "Patient/example" }, "requester": { "reference": "PractitionerRole/referring-clinician" }, "performer": [{ "reference": "Organization/receiving-clinic" }], "authoredOn": "2026-10-06T16:00:00Z", "reasonCode": [{ "text": "Clinical reason documented by the referring clinician" }] }, "request": { "method": "POST", "url": "ServiceRequest", "ifNoneExist": "identifier=https%3A%2F%2Fexample.org%2Freferrals%7CREF-2026-001" } }, { "fullUrl": "urn:uuid:74418b81-843b-4763-8b30-90782d85f290", "resource": { "resourceType": "Task", "identifier": [{ "system": "https://example.org/referral-work", "value": "REF-2026-001-coordinate" }], "status": "ready", "intent": "order", "code": { "text": "Coordinate referral through returned-note review" }, "focus": { "reference": "urn:uuid:2bcb56d0-993c-4e86-9ae2-3ca0c5dfa093" }, "for": { "reference": "Patient/example" }, "owner": { "reference": "Organization/referral-coordination" } }, "request": { "method": "POST", "url": "Task", "ifNoneExist": "identifier=https%3A%2F%2Fexample.org%2Freferral-work%7CREF-2026-001-coordinate" } } ] } ``` Submit with `medplum.executeBatch` using the transaction Bundle, and read the resulting resource locations from the transaction response. The SDK method name does not change the Bundle's transaction semantics. See [FHIR Batch and Transaction Requests](/docs/fhir-datastore/fhir-batch-requests). Conditional creates make replaying this initial submission safe for FHIR resource creation. A match leaves the existing resource unchanged. Handle a multiple-match error as a data-quality issue rather than silently picking a request. For the remaining snapshots, assume the transaction returned `ServiceRequest/referral-001` and `Task/coordinate-001`. Each snapshot shows a later persisted state in the workflow. ### 2. Store the Referral Package Upload the reviewed PDF bytes with `createBinary` or `createAttachment`, setting `securityContext` to the DocumentReference that controls access. You can also use `createDocumentReference` to perform the document creation, upload, and attachment update. See [Recipients and Referral Packages](/docs/careplans/referrals/recipients-and-packages). After uploading, the DocumentReference looks like this. `Binary/package-pdf-001` is the ID returned by the upload; no base64 is embedded in the attachment. ```json { "resourceType": "DocumentReference", "id": "package-001", "status": "current", "docStatus": "final", "type": { "text": "Referral package" }, "subject": { "reference": "Patient/example" }, "author": [{ "reference": "PractitionerRole/referring-clinician" }], "content": [{ "attachment": { "contentType": "application/pdf", "url": "Binary/package-pdf-001", "title": "Referral package REF-2026-001" } }], "context": { "related": [{ "reference": "ServiceRequest/referral-001" }] } } ``` The upload may use the DocumentReference as security context before the final attachment URL is populated. Establish the document's access rules first and recover incomplete uploads explicitly. A document's `final` status describes the document; it does not complete the referral. ### 3. Record the Thread and Sent Message The selected destination is `Organization/receiving-clinic`. The Communication header below groups the exchange and contains all participants, including its creator. ```json { "resourceType": "Communication", "id": "referral-thread", "status": "in-progress", "subject": { "reference": "Patient/example" }, "topic": { "text": "Referral REF-2026-001" }, "about": [{ "reference": "ServiceRequest/referral-001" }], "sender": { "reference": "Organization/referral-coordination" }, "recipient": [ { "reference": "Organization/referral-coordination" }, { "reference": "Organization/receiving-clinic" } ] } ``` The sent child message carries the package. Record `sent` after the send event and retain a stable message identifier. ```json { "resourceType": "Communication", "id": "send-001", "identifier": [{ "system": "https://example.org/referral-messages", "value": "TX-001" }], "status": "in-progress", "subject": { "reference": "Patient/example" }, "partOf": [{ "reference": "Communication/referral-thread" }], "about": [{ "reference": "ServiceRequest/referral-001" }], "sender": { "reference": "Organization/referral-coordination" }, "recipient": [ { "reference": "Organization/receiving-clinic" } ], "sent": "2026-10-06T17:00:00Z", "payload": [{ "contentReference": { "reference": "DocumentReference/package-001" } }] } ``` :::note[Thread participants and delivery destinations serve different purposes] Here, both teams participate in an in-app conversation. For external delivery, the integration resolves the intended destination separately from the complete local participant list and retains the provider's transmission identifier. ::: ### 4. Accept the Work and Assign Scheduling Conditionally create the receiving team's fulfillment Task with `status: requested`, using the stable identifier below. When the team agrees to perform the work, version-check the update to `accepted`. The snapshot after that acknowledgment is: ```json { "resourceType": "Task", "id": "fulfill-001", "identifier": [{ "system": "https://example.org/referral-work", "value": "REF-2026-001-fulfill" }], "status": "accepted", "intent": "order", "code": { "text": "Fulfill specialist consultation" }, "focus": { "reference": "ServiceRequest/referral-001" }, "for": { "reference": "Patient/example" }, "requester": { "reference": "Organization/referral-coordination" }, "owner": { "reference": "Organization/receiving-clinic" } } ``` Once acceptance and required authorization checks are recorded, conditionally create a separate scheduling Task. Its completion criterion is a confirmed booking: ```json { "resourceType": "Task", "id": "schedule-001", "identifier": [{ "system": "https://example.org/referral-work", "value": "REF-2026-001-schedule" }], "status": "ready", "intent": "order", "code": { "text": "Schedule specialist consultation" }, "focus": { "reference": "ServiceRequest/referral-001" }, "for": { "reference": "Patient/example" }, "owner": { "reference": "Organization/receiving-clinic" } } ``` The referring team's `coordinate-001` Task owns follow-through and returned-note review. The receiving team's fulfillment and scheduling Tasks are independent obligations linked through the same ServiceRequest, outside that coordination tree. Different owners can share a Task hierarchy when the workflow calls for it; this example keeps the two teams' obligations separate. The shared view can now show receiving-team acceptance independently of the message's sent time. See [status updates](/docs/careplans/referrals/processing-and-coordination#keep-the-referring-team-in-the-loop) for notifying the referrer. ### 5. Link the Booking The receiving team's booking workflow creates an Appointment. This snapshot omits scheduling extensions and Slot details; use the [Scheduling operations](/docs/scheduling) to create the booking for your configured service. ```json { "resourceType": "Appointment", "id": "consultation-001", "status": "booked", "basedOn": [{ "reference": "ServiceRequest/referral-001" }], "start": "2026-10-13T16:00:00Z", "end": "2026-10-13T16:30:00Z", "participant": [ { "actor": { "reference": "Patient/example" }, "status": "accepted" }, { "actor": { "reference": "PractitionerRole/receiving-clinician" }, "status": "accepted" } ] } ``` Version-check the update to `Task/schedule-001`, setting `status: completed` and `output` to a typed Appointment reference: `type.text = "Confirmed booking"` and `valueReference.reference = "Appointment/consultation-001"`. Notify the referring team of the booking. The overall coordination Task stays open because its scope includes the returned-note review. ### 6. Retain and Review the Returned Note The receiving team moves its fulfillment Task to `in-progress` when that work begins, then to `completed` when its consultation criteria are met. After the service, upload the returned note using the same Binary pattern. Preserve the author and match the source referral identifier before linking it. ```json { "resourceType": "DocumentReference", "id": "consultation-note-001", "status": "current", "docStatus": "final", "type": { "text": "Consultation note" }, "subject": { "reference": "Patient/example" }, "author": [{ "reference": "PractitionerRole/receiving-clinician" }], "content": [{ "attachment": { "contentType": "application/pdf", "url": "Binary/consultation-note-pdf-001", "title": "Consultation note REF-2026-001" } }], "context": { "related": [{ "reference": "ServiceRequest/referral-001" }] } } ``` Create the review Task once for the relevant note/version. The referring clinician reviews the specialist's note here. The review annotation is signed by `Practitioner/referring-clinician`, the Practitioner behind the Task owner's PractitionerRole; the consultation document retains the specialist as its author. This example shows it after the reviewer has completed their work: ```json { "resourceType": "Task", "id": "review-note-001", "identifier": [{ "system": "https://example.org/referral-work", "value": "REF-2026-001-review-note-v1" }], "status": "completed", "intent": "order", "code": { "text": "Review consultation note" }, "partOf": [{ "reference": "Task/coordinate-001" }], "focus": { "reference": "ServiceRequest/referral-001" }, "for": { "reference": "Patient/example" }, "owner": { "reference": "PractitionerRole/referring-clinician" }, "input": [{ "type": { "text": "Returned consultation note" }, "valueReference": { "reference": "DocumentReference/consultation-note-001" } }], "businessStatus": { "text": "Reviewed; no additional coordination needed" }, "output": [{ "type": { "text": "Review disposition" }, "valueCodeableConcept": { "text": "No additional coordination needed" } }], "note": [{ "authorReference": { "reference": "Practitioner/referring-clinician" }, "time": "2026-10-14T17:00:00Z", "text": "Reviewed the returned note and recorded the disposition." }] } ``` The responsible party reconciles the ServiceRequest's status from evidence that the requested service occurred. The coordinator completes the parent Task after the required note review and follow-up decisions are recorded. ### Show the Coordinator the Whole Referral ```http GET /fhir/R4/Task?focus=ServiceRequest/referral-001 GET /fhir/R4/Communication?part-of=Communication/referral-thread GET /fhir/R4/Appointment?based-on=ServiceRequest/referral-001 GET /fhir/R4/DocumentReference?related=ServiceRequest/referral-001 ``` These queries bring the work items, conversation, booking, and documents into one referral view. The Communication query uses the known thread ID to retrieve its messages. Use `part-of` for these messages; R4 defines `about` as a reference field without a corresponding standard search parameter. Follow pagination and run each query under the appropriate AccessPolicy. These are separate resource searches; a ServiceRequest search does not automatically return everything linked to the referral. When the receiving clinic uses another server, exchange identifiers and map references through the integration instead of assuming these local queries reach its records. --- ## Referrals A referral can be sent successfully and still go nowhere. The receiving clinic may need another document, the patient may need help scheduling, or the consultation note may never make it back to the referring team. A useful referral workflow keeps those next steps visible. In Medplum, start with a [ServiceRequest](/docs/api/fhir/resources/servicerequest) for the requested clinical service. Add Tasks for the work around it and link the messages, documents, appointments, and results as they arrive. :::tip[Plan the workflow first] Use the [Referrals Decision Guide](/docs/decision-guides/referrals) to decide which sending, receiving, and follow-up steps your product supports. You can build either side of the exchange without implementing every workflow in this section. ::: ### Decide Whether the Teams Share a Project For an internal referral, such as primary care referring to the practice's behavioral health team, both teams can use the same ServiceRequest. Create the receiving team's Task directly, assign its owner, and track acceptance and follow-up. Use Communications when the teams need to exchange context; a separate delivery workflow is optional. Configure access for each team's work and clinical records. For a referral to another system, add the transmission and intake steps: agree on identifiers and supported payloads, track delivery, resolve incoming patient references, and reconcile status updates. Each server uses its own local resource IDs while preserving the business referral identifier. The [worked example](/docs/careplans/referrals/fhir-resource-examples) uses two teams in one project so every reference can be followed locally. The same milestones apply across systems, with the integration carrying messages and status updates between them. ### Connect the Request to the Work Around It | Resource | Role in the workflow | | --- | --- | | `ServiceRequest` | The requested service, patient, requester, intended performer, and clinical context | | `Task` | An accountable work item, such as intake review, scheduling, or reviewing the returned note | | `QuestionnaireResponse` | Answers captured in a referral form, retained alongside the resulting request | | `Communication` | Messages about the referral, including the material sent and relevant transmission times | | `DocumentReference` and `Binary` | Document metadata and the actual file bytes | | `Appointment` and `Encounter` | The booking and the care interaction that follows | | `DiagnosticReport` or another clinical result | Evidence of the service provided | ```mermaid flowchart LR task[Task] -->|focus| referral[ServiceRequest] message[Communication] -->|about| referral message -->|payload.contentReference| document[DocumentReference] document -->|content.attachment.url| binary[Binary] appointment[Appointment] -->|basedOn| referral encounter[Encounter] -->|basedOn| referral result[DiagnosticReport] -->|basedOn| referral ``` You can follow these links from the clinical request to the work, messages, and evidence around it. Each patient-related resource also needs its own patient reference. The arrows in this diagram connect clinical and workflow context; access is enforced by [AccessPolicy and ProjectMembership](/docs/access/access-policies). ### Know What Each Milestone Actually Tells You A delivery receipt tells you that a package arrived. It does not tell you that the clinic accepted the referral or that the patient received the service. Track these milestones separately: | Milestone | Evidence | | --- | --- | | Package delivered | Channel acknowledgment correlated to the outbound message | | Referral accepted | Receiving team's explicit acceptance of the work | | Visit scheduled | Appointment linked to the ServiceRequest | | Service performed | Encounter or clinical result, with request status reconciled by the responsible party | | Loop closed | Returned information reviewed and any required follow-up assigned | `ServiceRequest.status` describes the clinical request. `Task.status` describes a particular unit of work. Use `Task.businessStatus` for local stages such as waiting for documents; keep the meaning of each stage consistent across the UI and automation. ### Build Your Referral Workflow 1. [Creating and Capturing Referrals](/docs/careplans/referrals/creation-and-capture): forms, draft orders, identifiers, and release. 2. [Recipients and Referral Packages](/docs/careplans/referrals/recipients-and-packages): directory choices, supporting context, and file handling. 3. [Sending Referrals and Tracking Delivery](/docs/careplans/referrals/transmition-and-tracking): channels, communication records, acknowledgments, and retries. 4. [Receiving and Triaging Referrals](/docs/careplans/referrals/receiving-and-triage): source retention, patient matching, duplicate handling, and review. 5. [Processing and Coordinating Referrals](/docs/careplans/referrals/processing-and-coordination): ownership, authorization, scheduling, and rerouting. 6. [Results and Closing the Loop](/docs/careplans/referrals/results-and-closure): matching returned records and defining completion. 7. [Worked Referral Example](/docs/careplans/referrals/fhir-resource-examples): a connected set of R4 resources. For patterns shared with other practice workflows, see [Care Coordination](/docs/careplans), especially [Tasks and Work Queues](/docs/careplans/tasks) and [Handoffs and Escalation](/docs/careplans/handoffs-and-escalation). --- ## Processing and Coordinating Referrals Once a referral is accepted, the next step may belong to a different team. Someone checks authorization, someone contacts the patient, and someone arranges the visit. A single "in progress" label rarely tells the coordinator enough to help. ### Give Each Obligation an Owner Use Tasks for steps with independent owners or completion criteria. Each patient-related Task has `for` referencing the Patient and `focus` referencing the ServiceRequest. Set `owner` to the accountable person or group; use `performerType` when eligible staff claim work from a role-based pool. | Work item | Completion evidence | | --- | --- | | Gather missing records | Required material linked in `input` or `output`, with review recorded | | Check authorization | Payer response or documented determination, including scope and validity | | Schedule the service | Appointment reference in `output` | | Review the returned note | Review disposition and any follow-up request in `output` | Every Task input or output needs a `type` and one `value[x]`. Use `valueReference` for linked resources. Define local work-type and stage codes in a governed CodeSystem when automation or search depends on them. You can also use a parent Task to track an overall coordination obligation, with step Tasks linked through `partOf`. Add this only when someone owns that broader obligation. Define how its state is derived so staff do not have to maintain two competing versions of referral progress. Use the [handoff pattern](/docs/careplans/handoffs-and-escalation#release-the-next-step) to start each next step. See [Teams and Delegation](/docs/careplans/teams-and-delegation) for role eligibility and [Handoffs and Escalation](/docs/careplans/handoffs-and-escalation) for claiming, deadlines, and exception handling. ### Find Out What Is Needed Before Booking Before telling the patient they are ready to book, establish which coverage or authorization checks your workflow requires. The payer integration may need to answer several different questions: | Question | Starting model | | --- | --- | | What benefits apply? | CoverageEligibilityRequest with `purpose: benefits`, linked to the patient's Coverage | | Is authorization required? | CoverageEligibilityRequest with `purpose: auth-requirements`, where supported by the payer | | Is the specific provider/service in network? | Authoritative payer directory or contract data, supplemented by applicable eligibility evidence | | What was submitted for authorization? | Claim with `use: preauthorization` when this matches the exchange contract | | What did the payer decide? | ClaimResponse and its authorization/adjudication details, mapped according to that contract | :::caution[A processed response is not an authorization approval] A successful transport response or `ClaimResponse.outcome: complete` is not by itself an approval. Interpret the payer's actual adjudication and authorization details, including the covered services, dates, units, and identifiers. Eligibility is also distinct from authorization approval. ::: The integration may use a payer API, an implementation guide, or an EDI mapping. Choose and validate that contract before defining the scheduling gate. These resources do not provide a universal clearinghouse connection or guarantee coverage. See the [R4 eligibility response fields](https://hl7.org/fhir/R4/coverageeligibilityresponse-definitions.html) for the information a response can carry. Keep the coordination Task on hold when required evidence is missing, with a reason and an accountable owner. Record a denial, expiration, or approved exception explicitly so it remains actionable. #### Carry Referral and Authorization Details into Billing When required by the payer, link the claim's `referral` to the ServiceRequest and put the issued authorization number in `Claim.insurance.preAuthRef`. The latter is a string, not a resource reference. See the [R4 Claim definitions](https://hl7.org/fhir/R4/claim-definitions.html#Claim.insurance.preAuthRef). Retain the authorization's effective period (`ClaimResponse.preAuthPeriod` when supplied), approved services, and any visit or unit limits under the payer's exchange contract. Check authorization dates and remaining authorized visits or units for each booking and claim. Keep payer authorization limits distinct from the clinician's requested service timing; an authorization expiring may require renewal while the clinical referral remains active. ### Book the Visit and Keep the Referral Linked Reference the ServiceRequest from `Appointment.basedOn` and, when the service occurs, `Encounter.basedOn`. Scheduling the appointment completes the scheduling work item; the clinical service still has to happen. Use the [Scheduling guides](/docs/scheduling) for availability and booking. Create Slots only for booked or blocked time, and validate practitioner eligibility through the controlled booking path. For patient outreach, use Communications linked to the request and a Task when a response needs tracking. The [UnityAI scheduling case study](/blog/scheduling-agents-unity-ai) shows how referral data can support outreach and scheduling. Automation still needs a route back to staff when a patient cannot be reached or a booking cannot proceed. ### Follow Through When Scheduling Does Not Succeed Before the first outreach attempt, agree on the contact channels, spacing and number of attempts, urgency exceptions, and who can decide to stop. Keep a coordinator accountable until the work is rescheduled, transferred, or explicitly closed. Record outreach attempts as Communications with `subject` for the patient and `about` for the ServiceRequest. Retain the channel, sender, time of the actual attempt, and its outcome. Use a stable attempt identifier so a retried integration event records the same attempt once. Link the records from the coordination Task as needed, and keep its local `businessStatus` current for the queue. The table below uses a scheduling Task whose completion criterion is a booking. Local stage and reason labels are examples to define in your workflow CodeSystem. | Outcome | Next action and owner | Status and notice to the referring team | | --- | --- | --- | | Patient cannot be reached | Coordinator records attempts and the next contact date. An authorized staff member reviews the attempt limit and any urgency exception before stopping. | Keep work open while outreach continues. If stopped without a booking, cancel the scheduling Task with an unable-to-reach reason and report the attempts and disposition. | | Patient declines | Record the patient's decision and route any required clinical discussion to its own review Task. | Cancel outstanding scheduling work with a patient-declined reason. Notify the referrer so the responsible clinician can decide what happens to the order. | | Patient misses the visit | Record `Appointment.status: noshow`; assign a new rescheduling Task linked to the same referral if follow-up is required. | Preserve the completed original booking Task. Report the missed visit and the rescheduling or stop decision. | | Referral reaches a local validity limit | Coordinator routes the request for renewal or closure review under the practice's policy. Distinguish the order's validity from an expired payer authorization. | Record expiration as a local disposition. Cancel outstanding booking work if the authorized decision is to stop, and notify the referrer of the reason and renewal path. | Use `statusReason` and an authored note to explain cancellation. Reserve `completed` for the work the Task actually promised to finish; a separate review Task may complete even when its decision is to stop scheduling. R4 Task and ServiceRequest have no `expired` status. Reconcile the clinical request through the responsible order authority, using `revoked` when the request is withdrawn. See [Task statuses](https://hl7.org/fhir/R4/codesystem-task-status.html). ### Keep the Referring Team in the Loop Agree on which events warrant an update: accepted, scheduled, declined, unable to schedule, missed visit, and closure are useful starting points. Include the referral identifier, event time, disposition or reason, and next owner or action. This lets the referring team help before a referral goes quiet. Within one project, expose the shared workflow state through authorized views and [notifications](/docs/careplans/automating-workflows#let-the-assignee-know). Across systems, exchange a correlated status message through the [transmission workflow](/docs/careplans/referrals/transmition-and-tracking), retaining the Communication and delivery history. A failed status notification leaves follow-up work for the integration owner; it should not erase the scheduling outcome. ### Change the Destination Without Losing the History If only the destination changes and the clinical order remains valid, the authorized workflow can update `ServiceRequest.performer`, resolve the old work items, and create work for the new destination. Preserve prior messages and notify affected parties through the integration. If a new clinical order replaces the old one, create a new ServiceRequest with `replaces` referencing the prior request and end the old request appropriately, such as `revoked` when withdrawn. A receiver declining work does not automatically revoke the referring clinician's order. Use `Task.status: cancelled` only for the work being cancelled, with a reason. :::note[Choose the rerouting boundary] Whether a change of recipient is an amendment or a replacement depends on order authority and partner requirements. Agree on that rule with your clinical and integration teams, then apply it consistently. Both paths preserve the original transmission history. ::: Continue with [Results and Closing the Loop](/docs/careplans/referrals/results-and-closure). --- ## Receiving and Triaging Referrals An incoming referral may be a clean API request or a scanned document with a hard-to-read patient name. Both need an accountable intake path. Preserve what arrived, confirm who it belongs to, and make the next action clear to the receiving team. ### Keep What Arrived Store incoming files as Binary with a DocumentReference describing the source and receipt context. Retain sender identifiers and transmission identifiers separately: the same referral can arrive through more than one channel. For structured FHIR, validate the resource and resolve its references before persisting it locally. For C-CDA or other structured documents, parse the source fields directly. Image-only documents may need OCR and human review before the extracted values become clinical records. The [Titan case study](/blog/titan-case-study) illustrates why normalization and review matter in referral intake. Automated extraction can reduce transcription work, but the source remains useful when a reviewer needs to resolve an ambiguity. ### Confirm Which Patient the Referral Belongs To An uncertain match needs a place to wait and someone to resolve it. Keep that review separate from the clinical intake that follows: ```mermaid flowchart TD source[Document and intake Task] --> match{Patient identified?} match -->|No| review[Restricted identity review] review -->|Match resolved| linked[Link confirmed patient] match -->|Yes| linked linked --> request[Reconcile ServiceRequest] request --> triage[Assigned triage Task] ``` Match against trusted identifiers with their assigning namespaces. When identifiers are missing or conflict, send the item through a defined review process. Similar names and dates of birth can support a match decision; they are not a safe substitute for resolving it. If the patient is not yet identified, create an intake Task focused on the source DocumentReference. Omit `Task.for` and `DocumentReference.subject` until the match is established, and protect both through an organization-scoped intake AccessPolicy. Binary uses the corresponding security context. These unmatched records will not appear in a patient-compartment view. After confirmation, add the patient references to the intake records and create or reconcile the ServiceRequest. Set `ServiceRequest.subject` to the confirmed patient. :::tip[Identity and duplicate handling are different steps] A conditional create can keep the same external referral number from producing two ServiceRequests. It cannot decide whether two similar demographic records represent the same person. Resolve identity first, then use stable identifiers to make ingestion repeatable. ::: ### Give the Intake Team a Clear Next Step Preserve the sender's request semantics, including the clinical requester, service, priority, and identifiers. Use a receiving-side Task for triage, focused on the ServiceRequest and assigned to the intake organization, service, or individual. | Intake outcome | Representation | | --- | --- | | Claimed for evaluation | `Task.status: received` when a potential performer has claimed the work and is deciding whether to accept it | | Accepted by the receiving team | `Task.status: accepted`, with an accountable owner | | Review has begun | `Task.status: in-progress` | | Waiting for missing material | `Task.status: on-hold`, local `businessStatus`, and an explanatory `statusReason` | | Receiving team declines the work | `Task.status: rejected` with a reason and a response to the sender | These statuses belong to the Task whose requested work is being accepted or declined. If you model triage as its own review Task, that review can be `completed` with a declined disposition while the fulfillment request remains unaccepted. Choose the scope of each Task before defining its transitions. Reconcile the clinical order through the [agreed request-authority and rerouting rules](/docs/careplans/referrals/processing-and-coordination#cancel-or-reroute-deliberately). ### Recognize a Repeat Delivery or a Changed Request Use the source referral identifier, including its namespace, for conditional create. Use a stable identity for the initial receiving Task as well. Preserve each incoming transmission even when it relates to an existing request. If a second document lacks a reliable referral identifier, flag a potential duplicate for review. Patient plus specialty alone is not unique: a patient can have more than one legitimate referral for the same service. :::caution[An amendment needs review, even when the ID matches] An amendment needs a version-checked update or an explicit replacement workflow. Conditional create only finds an existing match; it does not merge new clinical content. Record who reviewed the amendment and retain the source evidence. ::: Send acknowledgments through the [transmission workflow](/docs/careplans/referrals/transmition-and-tracking), then move accepted work into [Processing and Coordination](/docs/careplans/referrals/processing-and-coordination). --- ## Recipients and Referral Packages "Send it to cardiology" might mean a named clinician, a clinic, or an intake team that decides who will see the patient. Model the destination at the level your workflow actually knows. You can assign the person who processes the referral separately. ### Choose Where the Referral Is Going | Destination | Recommended representation | | --- | --- | | A clinician in a particular practice or service | `ServiceRequest.performer` references PractitionerRole | | A named clinician without role-specific context | `ServiceRequest.performer` references Practitioner | | A clinic or organization | `ServiceRequest.performer` references Organization | | A service that assigns its own clinicians | `ServiceRequest.performer` references HealthcareService | | A requested physical site | `ServiceRequest.locationReference` references Location | | The person or team coordinating the next step | `Task.owner`, independently of the clinical performer | Use structured references in the recipient picker. When a directory comes from another system, preserve its identifiers and resolve the selected entry to the appropriate local resource. Do not assume an external resource ID is valid on your Medplum server. :::note[Preferred does not necessarily mean in network] A preferred-provider list expresses your practice's preference. Payer network participation and the patient's covered benefits need their own authoritative sources. Record the source and effective date of network information, and handle unknown or stale results explicitly. An eligibility response alone may not establish that a particular provider, location, and service are in network. ::: ### Send What the Receiving Team Needs Start with the receiving team's requirements, then let the sender review the material before release. Typical context includes the clinical reason, relevant results, current treatment, and coverage information. Select the records needed for the service instead of exporting the entire chart by default. Give the reviewer a path back to the evidence. Use `reasonReference` for the clinical reason and `supportingInfo` for additional context such as Observations, QuestionnaireResponses, and DocumentReferences. A request can reference structured evidence while also carrying a readable summary. `ServiceRequest.note` can carry narrative for the receiving team, and `patientInstruction` can carry instructions written for the patient. These fields do not create different access permissions within the resource. Keep internal-only material in separately protected resources and exclude it from the outbound package. ### Upload the File and Keep Its Metadata Store file bytes in Binary and metadata in [DocumentReference](/docs/fhir-datastore/external-documents). The attachment's `url` points to `Binary/{id}`. Use `createBinary` or `createAttachment` with a `securityContext` appropriate to the document's access policy; see [Binary Data](/docs/fhir-datastore/binary-data). Medplum also provides `createDocumentReference`, which creates the document record, uploads the Binary, and links the attachment. By default, it uses the DocumentReference as the Binary's security context. Supply the document's patient and other access-relevant fields when creating it. The helper performs multiple writes, so recover incomplete uploads before retrying the whole operation. :::caution[Keep file bytes out of the attachment] Use the Binary URL in `DocumentReference.content.attachment.url`. Do not embed base64 in `Attachment.data`. For outbound exchange, deliver the actual file or an authorized URL the receiving system can resolve; a relative Binary URL from your server is not sufficient on its own. ::: ### Keep a Copy of What the Recipient Saw A patient's chart can change after the referral is sent. Retain the generated package as a DocumentReference and link that exact document from the outbound Communication. For structured exchanges, retain the transmitted payload and correlation metadata according to your retention policy, including the resource versions represented. If staff correct and resend a package, preserve the first transmission and record the new one. A mutable `supportingInfo` list alone cannot tell you what the receiving clinic saw last Tuesday. Next, [send the referral and track delivery](/docs/careplans/referrals/transmition-and-tracking). --- ## Results and Closing the Loop The specialist saw the patient, but the referring team is still waiting for the note. From one team's perspective the service is complete; from the other's, there is work left to do. Model both so that a referral doesn't disappear from the queue too early. ### Bring the Note Back to the Original Referral | Returned record | Link to the original referral | | --- | --- | | Diagnostic result | `DiagnosticReport.basedOn` references ServiceRequest | | Completed consultation | `Encounter.basedOn` references ServiceRequest | | Consultation note or PDF | `DocumentReference.context.related` references ServiceRequest; attachment URL references Binary | | Message from the receiving team | `Communication.about` references ServiceRequest, with returned documents in the message payload | For a DocumentReference, use `context.encounter` only for an Encounter or EpisodeOfCare. A referral reference belongs in `context.related`. The [R4 DocumentReference definitions](https://hl7.org/fhir/R4/documentreference-definitions.html#DocumentReference.context.related) describe this distinction. Preserve the patient, author, clinical status, and relevant times on each returned record. If a DiagnosticReport has a rendered report, its `presentedForm` can use the [Binary URL pattern](/docs/careplans/referrals/recipients-and-packages#store-documents-with-binary). ### Check Which Referral the Result Belongs To Prefer source referral identifiers and the correlation established during exchange. Verify the patient and service context as well. A patient can have several open referrals, so matching only on patient or specialty is insufficient. When the identifiers are missing or contradictory, preserve the returned document and create review work before attaching it to a clinical request. Keep uncertain material in the restricted intake workflow described in [Receiving and Triage](/docs/careplans/referrals/receiving-and-triage). Deduplicate repeated deliveries using stable source identifiers. A corrected report is a revision to evaluate, not necessarily a duplicate to discard; preserve its relationship to the prior document or result. ### Review the Result and Assign Follow-Up Create a review Task when the referring team must acknowledge or act on returned information. Its `focus` can reference the ServiceRequest, and `input` can reference the returned DocumentReference or DiagnosticReport. Assign the eligible reviewer and populate `for` for the patient. Record the review disposition and rationale. Reference any resulting clinical request in `output`, and track its fulfillment separately. A clinician may finish reviewing a note while leaving a new follow-up obligation for the coordinator. Use a stable identifier for the review work, derived from the referral and returned result/version or another defined review occurrence. Repeated delivery of the same report reuses that review work. Define when an amended report warrants another review. ### Decide When Each Team Is Done The two teams finish at different points. In a workflow that includes returned-note review, the referring team still has work after the consultation: ```mermaid sequenceDiagram participant Referring as Referring team participant Receiving as Receiving team Referring->>Receiving: Send referral and package Receiving-->>Referring: Accept referral Receiving-->>Referring: Confirm booking Note over Receiving: Perform the service Receiving->>Referring: Return consultation note Note over Referring: Review note and assign any follow-up Note over Referring: Close coordination when its criteria are met ``` For declined, missed, or unscheduled referrals, use the [unsuccessful scheduling workflow](/docs/careplans/referrals/processing-and-coordination#follow-through-when-scheduling-does-not-succeed) and send its disposition back to the referring team. | Record | What completion means | | --- | --- | | Scheduling Task | The required booking has been made | | Fulfillment Task | The requested work represented by that Task has finished | | ServiceRequest | The requested service has been performed, reconciled through the responsible request authority | | Results-review Task | The returned evidence has been reviewed and required next steps recorded | | Overall coordination Task, if used | The practice's defined closure criteria have been met | Use an appropriate terminal `Task.status` as well as the local `businessStatus`. A business-stage label alone does not close the Task. If the service was declined or cancelled, record that outcome rather than marking the ServiceRequest completed. :::tip[Make the closure boundary visible to staff] For closed-loop workflows, completion usually needs both returned evidence and review. For outbound tracking that ends at acceptance, document that narrower boundary clearly in the product and reports. ::: ### Follow Up When the Note Is Missing or Changes A visit may be over while the note is still missing. Give that missing result an owner and a deadline. A scheduled Bot can identify work past its agreed deadline and create or escalate a follow-up obligation once. Follow the [Medplum deadline convention](/docs/careplans/handoffs-and-escalation#represent-time-accurately), including pause rules and date-range queries. When new information arrives after closure, preserve the original completion history. Create a new review Task for genuinely new work, or reopen an existing Task only under a defined lifecycle policy. Report the additional interval separately; `lastModified` alone cannot tell you how long either interval lasted. See [Operational Reporting](/docs/careplans/operational-reporting) for queue age, turnaround time, and completion measures. --- ## Sending Referrals and Tracking Delivery A fax receipt is useful evidence that something was delivered. The coordinator still needs to know whether the clinic accepted the referral and what happens next. Keep transport progress separate from the receiving team's clinical and operational decisions. ### Agree on What the Other System Can Receive | Channel | Implementation considerations | | --- | --- | | FHIR API | Agree on supported profiles, identifiers, authentication, reference resolution, and acknowledgment behavior with the peer | | PDF over an external channel | Generate a reviewed package, store it as DocumentReference/Binary, and send through the configured integration | | C-CDA exchange | Use a generator and validator appropriate to the agreed document template and receiving system | | In-app messaging | Use Medplum's Communication thread and message model, with referral context and document references | A Bot can coordinate generation and transmission. For C-CDA, follow the [C-CDA integration guidance](/docs/integration/c-cda) and verify the capabilities of your chosen generator; producing XML does not establish template conformance. Across systems, preserve the referral's business identifier and map local resource references. Agree on which party owns each update. Two servers may represent the same business request with different resource IDs. ### Keep the Package with Its Conversation Follow the [Messaging Data Model](/docs/communications/messaging-data-model): - A thread header groups the conversation and has no `payload` or `partOf`. - Each message has `partOf` pointing to the header and its own payload. - Include all participants, including the sender or thread creator, in `recipient` on the thread header. A child message identifies its intended recipients. - Set patient context on the header and each patient-related message. Access does not propagate through `partOf`. - Use `about` to reference the ServiceRequest and `payload.contentReference` to reference the DocumentReference actually sent. In Medplum's messaging convention, a sent message uses `status: in-progress` with `sent` populated. This is the message lifecycle, not the referral's acceptance or completion state. Track read receipts according to the [message-status guide](/docs/communications/read-receipts-and-message-status). The [Worked Referral Example](/docs/careplans/referrals/fhir-resource-examples) includes a thread, message, and document linked to the same request. ### Match Delivery Receipts to the Right Send When a delivery callback arrives, the coordinator should be able to trace it to the exact package that was sent. Persist the external provider's message or transmission identifier in a namespaced `Communication.identifier`. Correlate callbacks to that identifier and retain the provider's evidence. Populate `received` only when the acknowledged event means receipt under the channel's agreed contract. Use a delivery Task when staff need to own failures or missing acknowledgments. Its completion criterion might be confirmed delivery; a separate receiving-side Task records acceptance and processing. Avoid interpreting a successful API response as acceptance unless the integration contract explicitly gives it that meaning. For callbacks, validate the source, deduplicate events, and handle out-of-order updates. A late delivery failure should not overwrite a later confirmed outcome without reconciliation. ### Retry Without Sending Duplicates Before the external call, persist the intended send and a stable correlation key. Use the transport provider's idempotency feature when available. If a timeout leaves the outcome unknown, check the provider's state or route the item to staff before sending again. :::caution[Check an uncertain send before trying again] A FHIR transaction can make linked resource writes atomic. It cannot undo a fax or email already sent by another system. See [Automating Care Workflows](/docs/careplans/automating-workflows) for the distinction between repeatable FHIR writes and external side effects. ::: Next, handle [receiving and triage](/docs/careplans/referrals/receiving-and-triage), or continue outbound follow-up with [Processing and Coordination](/docs/careplans/referrals/processing-and-coordination). --- ## Tasks and Work Queues import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import ExampleCode from '!!raw-loader!@site/..//examples/src/careplans/tasks.ts'; Calling a patient, reviewing an assessment, and checking an authorization all need someone to own the next step. Use [Task](/docs/api/fhir/resources/task) to represent each unit of work, including its owner, status, and completion evidence. Keep the clinical request and resulting record linked to the Task so the team can see both the work and its context. For role eligibility and clinical review, see [Teams and Delegation](/docs/careplans/teams-and-delegation). For dependencies, deadlines, and completion criteria, see [Handoffs and Escalation](/docs/careplans/handoffs-and-escalation). The Medplum [Clinical Task Management Demo](https://github.com/medplum/medplum-task-demo) provides an in-depth reference implementation of a task management system that addresses these concerns. ### Start with One Clear Unit of Work Start with an action someone can finish: call the patient, review the result, or collect a missing document. A Task gives that action an owner and a lifecycle. Its links give the assignee the clinical context they need to do the work. For example, a [`Task`](/docs/api/fhir/resources/task) might represent the task of having a practitioner complete a [PHQ-9 questionnaire](https://www.apa.org/depression-guideline/patient-health-questionnaire.pdf) for a patient as part of their onboarding. A common application is for organizations to build **task queue systems** to route tasks to the correct practitioner based on specialty, level of credential, and availability. The [Medplum Task Demo](https://github.com/medplum/medplum-task-demo) application provides a minimalist task queue that demonstrates task search, assignment, and status. For real-time task updates, consider using [Subscriptions](/docs/subscriptions). ### Name the Work to Be Done Use `Task.code` to describe the kind of work. A title such as "Review intake assessment" tells the assignee what to do. Reusing a consistent code for that work type also lets you find all assessment reviews in one query. Use a verified standard code when it matches the work. Otherwise, start with `Task.code.text` or define a local CodeSystem for work types that need consistent search and automation. `Task.description` can be used to add additional descriptive text to the specific [`Task`](/docs/api/fhir/resources/task) instance. **Example: ** ```ts { resourceType: 'Task', id: 'example-task', code: { text: 'Complete PHQ-9', }, description: "Patient to complete PHQ-9 depression screening", //... } ``` ### Show Where the Work Stands A coordinator should be able to tell whether work is ready, underway, or waiting on someone else. Start with those operational questions when choosing your status transitions. [`Task`](/docs/api/fhir/resources/task) provides three fields: `status`, `businessStatus`, and `statusReason`. `Task.status` maps to the FHIR task lifecycle shown below. It provides coarse-grained information about the activity state of a [`Task`](/docs/api/fhir/resources/task) and is most useful for day-to-day operations, as it allows for efficient queries on active, completed, and cancelled tasks. These queries will remain stable as your implementation scales. Use `requested`, `received`, `accepted`, and `rejected` when the workflow includes a request/acknowledgment exchange, including within one system. Internal actionable work can start at `ready`. ![Task lifecycle](./task-state-machine.svg) `Task.businessStatus` should map to your implementation's specific operational funnel. It provides fine-grained information to help customer service and operations teams troubleshoot tasks and monitor progress. It is also useful for analytics teams to compute conversion metrics between pipeline stages. `Task.statusReason` describes _why_ the [`Task`](/docs/api/fhir/resources/task) has the current status, and is most commonly used when `status` is set to `"on-hold"` or `"cancelled"`. Using an orthogonal `statusReason` allows operations teams to efficiently query for all tasks at the same point in the funnel, while analytics teams can further break down by all the reasons they may be on hold. ### Make Urgency Visible `Task.priority` can be used to indicate the urgency of the task. This field uses a fixed set of codes that are borrowed from acute in-patient care settings. | **Code** | **Definition** | | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | [`routine`](https://hl7.org/fhir/R4/codesystem-request-priority.html#request-priority-routine) | The request has normal priority. | | [`urgent`](https://hl7.org/fhir/R4/codesystem-request-priority.html#request-priority-urgent) | The request should be actioned promptly - higher priority than `routine`. | | [`asap`](https://hl7.org/fhir/R4/codesystem-request-priority.html#request-priority-asap) | The request should be actioned as soon as possible - higher priority than `urgent`. | | [`stat`](https://hl7.org/fhir/R4/codesystem-request-priority.html#request-priority-stat) | The request should be actioned immediately - highest possible priority (i.e., an emergency). | While these terms might feel awkward in a digital health setting, Medplum recommends that implementations use these codes rather than create their own extensions in order to maintain interoperability with the ecosystem. ### Give the Task an Owner `Task.for` indicates who _benefits_ from the task, and is most commonly the patient for whom care is being delivered. `Task.owner` indicates the party responsible for _performing_ the task. This can be either: - An individual: [`Practitioner`](/docs/api/fhir/resources/practitioner), [`PractitionerRole`](/docs/api/fhir/resources/practitionerrole), [`Patient`](/docs/api/fhir/resources/patient), [`RelatedPerson`](/docs/api/fhir/resources/relatedperson), or - A group: [`Organization`](/docs/api/fhir/resources/organization), [`HealthcareService`](/docs/api/fhir/resources/healthcareservice), [`CareTeam`](/docs/api/fhir/resources/careteam) You can search for all unassigned tasks using the [`:missing`](/docs/search/basic-search#missing) search modifier. **Typescript** {ExampleCode} **CLI** {ExampleCode} **cURL** {ExampleCode} #### Describe the Role Required Use `Task.performerType` for the kind of participant needed, such as a coordinator or clinical reviewer. Keep the role vocabulary consistent with your PractitionerRole model and verify that each code describes the actual role. A general assistant and a physician assistant, for example, have different responsibilities. ```ts { resourceType: 'Task', // Illustrative local vocabulary; replace the namespace with one you govern. performerType: [{ coding: [{ system: 'https://example.org/CodeSystem/workflow-roles', code: 'intake-coordinator', display: 'Intake coordinator', }], }], // ... } ``` The `example.org` namespace and code above define an illustrative local role, not a standard clinical code. Use the same coding in your role model and in the pool's token search: ```http GET /fhir/R4/Task?performer=https%3A%2F%2Fexample.org%2FCodeSystem%2Fworkflow-roles%7Cintake-coordinator&owner:missing=true&status=ready ``` `text` alone does not supply the `system|code` token used by this query. Add a work-type filter when the same role handles several queues. See [Teams and Delegation](/docs/careplans/teams-and-delegation) for eligibility and [Message Response Tracking and Routing](/docs/communications/message-response-tracking-and-routing) for a conversation-based work queue. ### Link the Record Someone Needs to Act On The `Task.focus` element tracks the FHIR resource being _operated on_ by this task, known as the "focal resource". See the [Examples](#examples) section below for examples of focal resources in common scenarios. When several people work on the same referral, their Tasks can all point to that ServiceRequest. Populating `focus` makes it possible to find those work items together and see where the referral is waiting. ### Set Start Dates and Due Dates For a Task seeking fulfillment of a request referenced by `focus`, the `Task.restriction.period` field describes the fulfillment window, with `Task.restriction.period.end` representing the _due date_, and `Task.restriction.period.start` representing the (potentially optional) start date. A task that cannot be started until November 1 and is due at the end of December looks like this: ```ts { resourceType: 'Task', status: 'requested', intent: 'order', focus: { reference: 'ServiceRequest/example' }, for: { reference: 'Patient/example' }, restriction: { period: { start: '2026-11-01T00:00:00.000Z', end: '2026-12-31T23:59:59.999Z', }, }, } ``` These guides use `restriction.period.start/end` for planned start and due dates in general work queues as well as request fulfillment. The broader use is an application convention; agree on its mapping when exchanging Tasks with another system. Use `executionPeriod` for actual work. See [Handoffs and Deadlines](/docs/careplans/handoffs-and-escalation#represent-time-accurately) for business calendars and pauses. #### Searching by due date range The `due-date` search parameter searches `Task.restriction.period`. Prefixes such as `le`, `ge`, `sa`, and `eb` are described in [Searching by comparison](/docs/search/basic-search#searching-by-comparison). With the default index, Medplum stores one timestamp for `due-date`: `period.start` when it is set, and `period.end` when the start is missing. Every prefix compares that single value. On the task above, `due-date=le2026-11-30` matches, because November 1 is on or before November 30, even though the due date is December 31. The same task matches `due-date=lt2026-12-01` as soon as November 1 has passed, so it shows up in an overdue list while it is still on time. Enable the [`range-search`](/docs/self-hosting/project-settings#project-feature-flags) project feature to compare both bounds. This allows for "start dates" or future scheduled tasks. A missing start is treated as the beginning of time, and a missing end as the end of time. A date with no time covers that calendar day, so `le2026-09-30` includes a start later on September 30 and excludes a start on October 1. :::tip[Medplum hosted service] If you use Medplum's hosted service, please email [support@medplum.com](mailto:support@medplum.com) to enable `range-search` for your project. ::: The examples below assume `range-search` is on and that today is 2026-09-30. **Hide tasks scheduled for the future.** Returns tasks whose start is today or earlier, including overdue tasks and tasks that have an end but no start. The sample task stays out until November 1. ``` GET [base]/Task?due-date=le2026-09-30 ``` Tasks with no `restriction.period` are omitted, because there is no period to compare. To keep them, combine the date test with a missing check in [`_filter`](/docs/search/filter-search-parameter): ``` GET [base]/Task?_filter=due-date le 2026-09-30 or due-date pr false ``` **Actionable today.** The period overlaps today: the start has arrived and the due date has not passed. The sample task is excluded in September, included through December, and excluded again in January. ``` GET [base]/Task?due-date=2026-09-30 ``` **Overdue.** The period ended before today. The sample task is excluded until after December 31. Use `eb` for this query. `lt` compares the start, so `due-date=lt2026-11-15` includes the sample task during November, while the due date is still in December. ``` GET [base]/Task?due-date=eb2026-09-30 ``` **Due on or before the end of November.** The period ends before December 1. The sample task is excluded. A task due on November 29 is included. ``` GET [base]/Task?due-date=eb2026-12-01 ``` **Scheduled for the future.** The start is after today. The sample task is included until November 1. ``` GET [base]/Task?due-date=sa2026-09-30 ``` ### Record When the Work Happened The `Task.executionPeriod` field describes the time period over which the [`Task`](/docs/api/fhir/resources/task) was actually actioned. Properly populating this field makes it easier to identify stalled tasks and compute turnaround-time metrics. `Task.executionPeriod.start` is used to store the start time of the _first action_ taken against this task. `Task.executionPeriod.end` is used to mark the completion time of the _final action_ taken against this task. ### Leave Context for the Next Person `Task.note` can be used to capture narrative text that is not represented elsewhere in the resource. The most common use for this field is to record comments from the task assignee as they work on the task. When used this way, it is a best practice to include `authorReference` or `authorString`, along with `time`, in the [`Annotation`](/docs/api/fhir/datatypes/annotation). ### Break Larger Jobs into Subtasks `Tasks` can be organized into a hierarchical structure to create subtasks. To represent this hierarchy, subtasks should reference their parent using the `Task.partOf` element. `Task.partOf` is a searchable field that can be used to query all sub-tasks of a given task, and can be combined with the [`_revinclude`](/docs/search/includes#_include-and-_revinclude) and [`:iterate`](/docs/search/includes#iterate-modifier) directives to query the entire [`Task`](/docs/api/fhir/resources/task) tree. :::caution[Start with a shallow Task hierarchy] While task hierarchy functionality is powerful, it can be complex to maintain and operationalize. Medplum recommends that most implementations start with a single-level [`Task`](/docs/api/fhir/resources/task) hierarchy and gradually add depth over time. ::: ### Examples | Use Case | Task Owner | Focal Resource | Example `businessStatuses` | Additional Info | | ----------------------------------------------- | ----------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | Complete patient intake questionnaire | New Patient. Care Coordinator | `Questionnaire` | Questionnaire sent to patient First reminder sent Second reminder sent Questionnaire completed by patient Responses reviewed by care coordinator | Use `Task.output` to reference resulting `QuestionnaireResponse` | | Review lab report | Physician | `DiagnosticReport` | Report Available Assigned to physician Reviewed by physician Discussed with patient | | | Verify patient identity (e.g. driver's license) | Patient. Care Coordinator | `DocumentReference` | Identification document requested Documentation received Documentation received Documentation verified | | | Complete encounter notes | Physician | `ClinicalImpression` | Encounter complete. Physician note required Note drafted Note finalized | Use `Task.encounter` to reference the original encounter | ### Build a Queue and Let Staff Claim Work A queue is a filtered view of Tasks, not a separate FHIR resource. Use `code` for work type, `status` for lifecycle, `businessStatus` for a local stage, `performerType` for the required role, and `owner` for current accountability. Keep a stable local coding namespace for operational concepts that do not have an appropriate verified standard code. ```http GET /fhir/R4/Task?owner=HealthcareService/intake&status=ready,on-hold GET /fhir/R4/Task?owner:missing=true&status=ready GET /fhir/R4/Task?owner=PractitionerRole/coordinator&status=ready,in-progress ``` Add work-type and role filters for the actual pool, and follow pagination. Group-owned items are not returned by `owner:missing=true`; choose whether the group or an unassigned role pool owns the work before writing queue queries. :::caution[Two people may claim the same Task] To claim an item, read the Task, check its current state and the claimant's eligibility, then update `owner` with `If-Match` for the version read. A `412 Precondition Failed` means the record changed; reload and let the user act on the current state. Do not automatically overwrite another claimant. See [Version Checking](/docs/fhir-datastore/updating-data#preventing-lost-updates-with-version-checking). ::: Use `Task.for` for the patient beneficiary, and set the clinical request's own patient reference separately. Non-patient work can omit `for` when there is no beneficiary. Configure [assignment and access rules](/docs/careplans/teams-and-delegation#assignment-and-access) for each queue. For work arising from a conversation, see [Message Response Tracking and Routing](/docs/communications/message-response-tracking-and-routing). Use [Operational Reporting](/docs/careplans/operational-reporting) for stage durations and queue aging. The most recent modification time is not necessarily when an item entered its current stage. ### Give Patients and Caregivers Their Own Work List A patient completing intake has a different job from the clinician reviewing the answers. Give each a separate Task so the portal can show the patient's next action while staff track review in their own queue. For a questionnaire assignment, set `owner` and `for` to the Patient and `focus` to the Questionnaire. This example shows the assignment after submission: ```json { "resourceType": "Task", "status": "completed", "intent": "order", "code": { "text": "Complete intake questionnaire" }, "owner": { "reference": "Patient/example" }, "for": { "reference": "Patient/example" }, "focus": { "reference": "Questionnaire/intake" }, "output": [{ "type": { "text": "Submitted answers" }, "valueReference": { "reference": "QuestionnaireResponse/intake-response" } }] } ``` The portal queries the authenticated participant's open assignments and displays the action, due date, and progress. Use the assignment's Questionnaire to render the form, retain the QuestionnaireResponse with the patient as `subject`, and complete the Task when the required submission criteria are met. If staff must review the answers, create a separate review Task focused on that response. For caregiver work, use a RelatedPerson owner and keep `Task.for` pointing to the patient. Configure the caregiver's permitted actions through [Access Policies](/docs/access/access-policies). The QuestionnaireResponse's `source` can identify the patient or caregiver who supplied the answers, while `author` identifies who recorded them. See [Questionnaires](/docs/questionnaires) for form rendering and response handling. ### See Also - The [FHIR Workflow Specification](http://hl7.org/fhir/R4/workflow.html) - [Medplum Task Demo](https://github.com/medplum/medplum-task-demo) - [Blog Post: Task Management Apps](/blog/task-management-apps#dashboards) - [Chart data model](/docs/charting/chart-data-model#encounter-centric-resources) --- ## Teams, Assignment, and Top-of-License Care A clinician shouldn't have to chase a missing intake form before reviewing it. A coordinator can prepare the information, and the clinician can make the clinical decision. Top-of-license care makes that division of work explicit, with the right qualifications and review at each step. Start with the work and eligibility rules agreed by the practice, then represent them in the resource model. The [Digital Health Operations blog post](/blog/digital-health-operations#top-of-license-care) explains why a shared task system is useful across clinical and administrative roles. ### Name the Team, the Role, and the Task Owner | Responsibility | Recommended representation | | --- | --- | | A practitioner's service and organizational role | `PractitionerRole`, linked to `practitioner`, `organization`, and the relevant `healthcareService` | | Ongoing participation in a patient's care | `CareTeam.subject` and `CareTeam.participant`, including `member`, `role`, and `period` | | Accountability for one action | `Task.owner`; use `performerType` to describe the required kind of participant | An organization-wide intake pool does not need a patient-specific CareTeam for every Task. Use an `Organization` or `HealthcareService` owner when the group is accountable, or an unowned role pool when eligible staff claim work. A patient CareTeam documents ongoing participation, including patients and caregivers when appropriate. Link an ongoing team through `CarePlan.careTeam` or `EpisodeOfCare.team`. A case coordinator can be represented by `EpisodeOfCare.careManager`. Keep today's Task owner explicit even when it is a member of that team. ### Let Coordinators Prepare and Clinicians Review Consider an assessment workflow: a coordinator checks completeness, then a clinician reviews the assessment and decides on follow-up. Use two Tasks because the owners and completion criteria differ. That gives each person a clear place to start and a clear definition of done. ```mermaid flowchart LR prepare[Coordinator prepares assessment] -->|Prepared material| review[Clinician reviews assessment] review --> decision{Follow-up needed?} decision -->|Yes| request[Authorized clinical request] decision -->|No| disposition[Record review disposition] ``` These arrows show the handoff your application manages. The resource links below retain the evidence for each step. | Step | Task context | Completion evidence | | --- | --- | --- | | Prepare the assessment | `for` references the patient; `focus` references the QuestionnaireResponse being prepared | `output` references the prepared QuestionnaireResponse or supporting document | | Review the assessment | `for` references the patient; `input` references the prepared material; `owner` identifies the reviewer | A documented review disposition, and references to resulting requests in `output` | Every input and output needs a `type` and one `value[x]`, for example `type.text = "Prepared assessment"` with `valueReference`. Text labels are readable examples; use a governed local CodeSystem when automation must recognize the meaning. Create the review Task when preparation completes, using a stable identifier and conditional create. Use `partOf` when both belong to a real parent Task, and implement the transition as described in [Handoffs](/docs/careplans/handoffs-and-escalation#release-the-next-step). :::caution[Keep clinical authorization with the reviewer] Completing preparation does not authorize a clinical service. Persist a clinical request through the authorized review/release path. Record the clinician or organization responsible for the request in its own requester/author fields; the Bot that saved it is not necessarily the clinical author. ::: ### Check Who Can Take the Work Use `Task.performerType` to classify the required role, then evaluate the candidate's current credentials, service enrollment, jurisdiction, availability, and supervision requirements in the controlled assignment path. A matching role code alone is insufficient. For multi-state services, follow [State-by-State Licensure](/docs/scheduling/state-by-state-licensure): a PractitionerRole represents a service or service line and can reference multiple jurisdiction Locations. Scheduling operations do not enforce licensure; the application validates eligibility before offering or booking an appointment. Agree on which roles may perform each action with the practice, including any supervision requirements. Use those decisions to configure assignment rules and validated role codes. ### Hand Off Work Without Losing Its Owner Use a version-checked update when changing `Task.owner`. If another person has claimed the item, reload and evaluate the new state. Let the user act on the current assignment. For the same work item, reassignment normally updates the existing Task. Record a transfer reason in an authored, timestamped `note`, or explicit `Provenance` when a structured reason is needed. If the next person has a distinct obligation, create a separate Task with its own completion evidence. If no eligible assignee is available, put the Task `on-hold`, explain why in `statusReason`, and assign an accountable escalation owner. A local `businessStatus` can identify the exception queue. ### Configure Who Can See the Work Enforce resource access with [Access Policies](/docs/access/access-policies) and ProjectMembership, including what the former owner can read after transfer. Assignment records responsibility; permission rules determine visibility for the Task and each linked resource. Keep patient references on each patient-related resource. For conversation-based work, use [Message Response Tracking and Routing](/docs/communications/message-response-tracking-and-routing). It applies these ownership rules while retaining the conversation's participants. --- ## Chart Data Model import ExampleCode from '!!raw-loader!@site/../examples/src/charting/representing-diagnoses.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; A charting UI reads patient-level data that persists across visits and encounter-level data created during each visit. This page is a reference: the first half covers longitudinal patient context (summary, demographics, devices), the second covers encounter-time resources (Observations, Conditions, Allergies, external documents). Each major resource has its own section. For visit orchestration and the SOAP-aligned template pattern, see [Visit Templates and the SOAP Approach](/docs/charting/visit-templates). ### Patient Summary and Queries Demographics come from the [`Patient`](/docs/api/fhir/resources/patient) resource. You can load related records with [Patient `$everything`](/docs/api/fhir/operations/patient-everything), but that response can be large for a dashboard view. Prefer targeted searches for active [`CarePlan`](/docs/api/fhir/resources/careplan), [`MedicationRequest`](/docs/api/fhir/resources/medicationrequest), [`Condition`](/docs/api/fhir/resources/condition), and other slices your UI needs. See [Search](/docs/search/) to compose queries. React components that help assemble timelines and summaries include [PatientTimeline](https://storybook.medplum.com/?path=/docs/medplum-patienttimeline--patient), [Timeline](https://storybook.medplum.com/?path=/docs/medplum-timeline--basic), [Search control](https://storybook.medplum.com/?path=/docs/medplum-searchcontrol--checkboxes), [ResourceAvatar](https://storybook.medplum.com/?path=/docs/medplum-resourceavatar--image), [FhirPathDisplay](https://storybook.medplum.com/?path=/docs/medplum-fhirpathdisplay--id), and tab navigation (for example [Mantine Tabs](https://mantine.dev/core/tabs/), used throughout Medplum apps). ```mermaid flowchart BT homer[
Patient
Homer Simpson
] obsA[
Observation
Heart Rate: 150 bpm
] obsB[
Observation
Pregnancy Status: 26 weeks
] condition[
Condition
Type II Diabetes
] allergy[
AllergyIntolerance
Penicillin G
critical
] medication[
MedicationRequest
Tylenol
] immunization[
Immunization
Fluvax
] risk[
RiskAssessment
Heart Attack: 10%
] obsA -->|subject| homer obsB -->|subject| homer condition -->|subject| homer allergy -->|patient| homer medication -->|subject| homer immunization -->|patient| homer risk -->|subject| homer ``` #### Key Resources for Patient Context | Resource | Description | | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | | [`Observation`](/docs/api/fhir/resources/observation) | Point-in-time clinical measurements and findings. | | [`Condition`](/docs/api/fhir/resources/condition) | Diagnoses and longitudinal problems. | | [`RiskAssessment`](/docs/api/fhir/resources/riskassessment) | Modeled risk scores and similar assessments. | | [`AllergyIntolerance`](/docs/api/fhir/resources/allergyintolerance) | Adverse reactions to drugs or substances. | | [`Immunization`](/docs/api/fhir/resources/immunization) | Vaccination history. | | [`Medication`](/docs/api/fhir/resources/medication) | Drug definitions; ordering uses [`MedicationRequest`](/docs/api/fhir/resources/medicationrequest) and summaries may use [`MedicationStatement`](/docs/api/fhir/resources/medicationstatement). | | [`MedicationAdministration`](/docs/api/fhir/resources/medicationadministration) | Documents when the patient received or took a medication (administration event). | #### Key Code Systems | Code System | Description | | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [LOINC](/docs/careplans/loinc) | Used in [`Observation`](/docs/api/fhir/resources/observation) and [`RiskAssessment`](/docs/api/fhir/resources/riskassessment) for clinical coding, compliance, billing, and reporting. | | [ICD-10](https://www.cdc.gov/nchs/icd/icd10cm_browsertool.htm) | Used in [`Condition`](/docs/api/fhir/resources/condition) for interoperability and billing; also common on encounter-related billing metadata. | | [RXNORM](/docs/medications/medication-codes#rxnorm) | Drug coding for allergies and medication orders. | | [SNOMED](https://www.snomed.org/) | Substances and clinical concepts. | | [CVX](https://www2a.cdc.gov/vaccines/iis/iisstandards/vaccines.asp?rpt=cvx) | Immunization vaccine types. | ### Encounter-Centric Resources During a visit, resources typically link to an [`Encounter`](/docs/api/fhir/resources/encounter). Notes often use [`ClinicalImpression`](/docs/api/fhir/resources/clinicalimpression); measurements use [`Observation`](/docs/api/fhir/resources/observation); orders use [`ServiceRequest`](/docs/api/fhir/resources/servicerequest) and [`MedicationRequest`](/docs/api/fhir/resources/medicationrequest). Note capture varies by product: some apps use a simple free-text area and persist narrative on [`Encounter`](/docs/api/fhir/resources/encounter) and/or [`ClinicalImpression`](/docs/api/fhir/resources/clinicalimpression). Others drive charting from a library of [Questionnaires](/docs/questionnaires/) and use [Bots](/docs/bots/) or [`$extract`](/docs/api/fhir/operations/extract) to create the right resources. Medplum is headless, so either pattern (or a mix) is valid – see [Visit Templates and the SOAP Approach](/docs/charting/visit-templates) for template-driven visits. ```mermaid flowchart BT app1[
Appointment
Homer Simpson
Fall Assessment
] encounter[
Encounter
Homer Simpson
Fall Assessment Encounter
] patient[
Patient
Homer Simpson
] condition[
Condition
Arthritis
] subgraph Results note[
Clinical Impression
Homer Simpson
Fall Assessment Note
] obs1[
Observation
Heart Rate: 150 bpm
] ra1[
RiskAssessment
Fall Risk: 80%
] end encounter -->|appointment| app1 encounter -->|subject| patient encounter -->|reasonReference| condition note -->|encounter| encounter obs1 -->|encounter| encounter ra1 -->|encounter| encounter ``` | Resource | Role | | ----------------------------------------------------------------- | ---- | | [`Encounter`](/docs/api/fhir/resources/encounter) | Visit container in-person or virtual. | | [`ClinicalImpression`](/docs/api/fhir/resources/clinicalimpression) | Assessment narrative and findings for the encounter. | | [`Condition`](/docs/api/fhir/resources/condition) | Encounter diagnoses when categorized accordingly. | | [`Observation`](/docs/api/fhir/resources/observation) | Vitals and other measurements tied to the encounter. | | [`RiskAssessment`](/docs/api/fhir/resources/riskassessment) | Structured risk scores for the encounter when used. | Encounter billing often references [CPT](https://www.ama-assn.org/practice-management/cpt/cpt-overview-and-code-approval) and [ICD-10](https://www.cdc.gov/nchs/icd/icd10cm_browsertool.htm) on billing-related structures; see [Provider visits](/docs/provider/visits) and [Billing](/docs/billing/). ### Patient Demographics Capturing patient demographics is a core EHR capability. ONC references this under [demographics (a)(5)](https://www.healthit.gov/test-method/demographics). Data may arrive via UI, integration, or API. The [USCDI](/docs/fhir-datastore/understanding-uscdi-dataclasses) V2 standard defines technical expectations; see the [US Core Patient profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-patient.html). #### React Components Medplum provides React components for [Patient demographics](https://storybook.medplum.com/?path=/story/medplum-resourceform--us-core-patient) aligned with common collection requirements. #### Capturing Aliases Use `Patient.name` with `period` and `use` to represent legal history – for example maiden names: ```json { "name": [ { "given": ["Marge", "Jacqueline"], "family": "Simpson", "period": { "start": "1980-01-01T00:00:00Z" }, "use": "official" }, { "given": ["Marge", "Jacqueline"], "family": "née Bouvier", "period": { "end": "1980-01-01T00:00:00Z" }, "use": "old" } ] } ``` #### Demographic Value Sets Use standard value sets rather than ad hoc codes. Correct coding supports reporting and interoperability. | Category | Name | | --------- | ----------------------------------------------------------------------------------------------------------------------------------- | | Gender | [Administrative Gender Value Set](https://hl7.org/fhir/R4/valueset-administrative-gender.html) | | Race | [US Core Race Extension](http://hl7.org/fhir/us/core/StructureDefinition/us-core-race) | | Ethnicity | [US Core Ethnicity Extension](http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity) | | Birth Sex | [Birth Sex Extension](http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex) | #### Lifecycle Managing duplicates belongs in every implementation – see [Patient deduplication](/docs/fhir-datastore/patient-deduplication). When a patient dies, record time of death on `Patient.deceased`. Represent cause of death as an [`Observation`](/docs/api/fhir/resources/observation) with ICD-10 or another appropriate ontology. #### Common Identifiers Capture identifiers such as driver license or national identifiers as namespaced identifiers on `Patient`. See [Naming data identifiers](/docs/fhir-basics#naming-data-identifiers). [Integration](/docs/integration) covers exchange-oriented lookups. #### Related Reading - [ONC (a)(5)](https://www.healthit.gov/test-method/demographics) - [Sample data](/docs/tutorials/importing-sample-data) - [US Core Patient Profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-patient.html) - [US Core Patient Storybook](https://storybook.medplum.com/?path=/story/medplum-resourceform--us-core-patient) - [ONC Certification for Medplum](/docs/compliance/onc) - [(a)(5) User Testing Video](https://youtu.be/NcxFl-GJ9Mc) :::caution[Pre-release] ONC (a)(5) certification is under development. ::: ### Implantable Devices Implantable device lists support safety, recalls, and adverse-event reporting. ONC describes expectations under [implantable device list (a)(14)](https://www.healthit.gov/test-method/implantable-device-list). Data may be captured via UI, integration, or API. See the [US Core implantable device profile](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-implantable-device.html) and [USCDI](/docs/fhir-datastore/understanding-uscdi-dataclasses). #### Device Identifiers and Safety Recording implantable devices supports: - Considering implants during treatment and testing - Finding patients affected by recalls - Supporting adverse-event reporting At minimum, record presence of the device and a coded device type. Also support collecting when applicable: - Device Identifier ([UDI-DI](https://www.fda.gov/medical-devices/global-unique-device-identification-database-gudid/accessgudid-public)) - Full UDI (human-readable or AIDC form) - Parsed production identifiers (manufacture date, expiration, lot, serial, distinct identifier) - MRI safety and latex presence where relevant #### React Components Medplum provides [implantable device React components](https://storybook.medplum.com/?path=/story/medplum-resourceform--us-core-implantable-device). List devices with a [search control](https://storybook.medplum.com/?path=/story/medplum-searchcontrol--checkboxes) over `Device`; see [search by reference](/docs/search/basic-search#searching-by-reference). A sample [FHIR Bundle](https://drive.google.com/file/d/1tLJ4qyWNczAvcfhxMA6HyKETdwFlDYjV/view?usp=sharing) illustrates implantable device data. #### Querying the FDA API Medplum can query the [FDA Device Lookup API](https://accessgudid.nlm.nih.gov/resources/developers/v3/device_lookup_api) to populate identifiers, manufacturer, and safety fields when a device identifier is supplied. #### Related Reading - [ONC (a)(14)](https://www.healthit.gov/test-method/implantable-device-list) - [Sample device bundle](https://drive.google.com/file/d/1tLJ4qyWNczAvcfhxMA6HyKETdwFlDYjV/view?usp=sharing) - [US Core Implantable Device](https://hl7.org/fhir/us/core/stu3.1.1/StructureDefinition-us-core-implantable-device.html) - [GUDID](https://www.fda.gov/medical-devices/global-unique-device-identification-database-gudid/accessgudid-public) - [Implantable Device Storybook](https://storybook.medplum.com/?path=/story/medplum-resourceform--us-core-implantable-device) - [User Safety Testing Script](https://docs.google.com/document/d/14D9ZZQW8kbdqHxEfskdXkaHkJVIx1RLJ/edit) - [(a)(14) video](https://youtu.be/DIjHguFUHB4) - [Device bot reference](https://github.com/medplum/medplum-ee/blob/main/packages/provider-app/src/device-bot.ts) ### Observations and Vital Signs Vital signs measure core physiological functions – blood pressure, pulse, respiration, temperature, and related values. Store them as [`Observation`](/docs/api/fhir/resources/observation) resources with coded `Observation.code` (typically [LOINC](/docs/careplans/loinc) per US Core expectations). #### Observation Elements | Element | Description | Code System | Example | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ------------------------- | | `status` | Preliminary, final, amended, cancelled, etc. | [Observation status](http://hl7.org/fhir/R4/valueset-observation-status.html) | registered | | `code` | What was observed. | [LOINC](/docs/careplans/loinc) | 8867-4 Heart Rate | | `subject` | Who or what the observation is about. | | Patient/homer-simpson | | `encounter` | Visit when observation was taken. | | Encounter/example | | `basedOn` | Plan or order that prompted the observation. | | CarePlan/example | | `performer` | Responsible party – patient for home readings, clinician or device in clinic. | | Practitioner/example | | `value[x]` | Result – see [Observation datatypes](#observation-datatypes). | | | | `dataAbsentReason` | Why a value is missing. | [Data absent reason](http://hl7.org/fhir/R4/valueset-data-absent-reason.html) | asked-but-unknown | | `interpretation` | High / low / normal categories. | [Interpretation](http://hl7.org/fhir/R4/valueset-observation-interpretation.html) | N | | `device` | Device used to produce the measurement. | | Device/apple-watch | | `specimen` | Specimen when lab-derived. | | Specimen/example | | `component` | Sub-results that belong together (for example BP systolic/diastolic). See [Multi-component observations](#multi-component-observations). | | |
Example: body temperature ```js { "resourceType": "Observation", "id": "example-observation-1", "code": { "system": "http://loinc.org", "code": "8310-5", "display": "Body temperature", }, "valueQuantity": { "value": 98.2, "unit": "degrees Fahrenheit", "system": "http://unitsofmeasure.org/", "code": "[degF]", }, "status": "final", } ```
Example: observation from survey input ```js { "resourceType": "Observation", "id": "example-observation-2", "code": { "system": "http://loinc.org", "code": "29463-7", "display": "Body weight", }, "valueQuantity": { "value": 165, "unit": "pounds", "system": "http://unitsofmeasure.org/", "code": "[lb_av]", }, "status": "preliminary", "method": { "system": "http://example-practice.org/", "code": "entry-survey", "display": "entry survey", }, } ```
Example: device-measured observation ```js { "resourceType": "Observation", "id": "example-observation-3", "code": { "system": "http://loinc.org", "code": "8480-6", "display": "Systolic blood pressure", }, "valueQuantity": { "value": 100, "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]", }, "status": "preliminary", "device": { "resource": { "resourceType": "Device", "id": "example-device", }, }, } ```
Example: patient-performed observation ```js { "resourceType": "Observation", "id": "example-observation-4", "code": { "system": "http://loinc.org", "code": "8867-4", "display": "Heart rate", }, "valueQuantity": { "value": 70, "unit": "beats per minute", "system": "http://unitsofmeasure.org", "code": "{Beats}/min", }, "status": "preliminary", "subject": { "resource": { "resourceType": "Patient", "id": "example-patient", }, }, "performer": { "resource": { "resourceType": "Patient", "id": "example-patient" }, }, } ```
:::tip[Units] `valueQuantity` uses FHIR Quantity: numeric `value` plus coded units when possible via [UCUM](https://ucum.org/). ::: #### Observation Datatypes | value[x] | Description | Datatype | Application | Example | | ---------------------- | --------------------------------------------------------------- | ----------------------------------------------------------- | --------------------- | -------------- | | `valueQuantity` | Numeric value with unit | [Quantity](/docs/api/fhir/datatypes/quantity) | Height | 177 cm | | `valueCodeableConcept` | Coded answer | [CodeableConcept](/docs/api/fhir/datatypes/codeableconcept) | Lab interpretation | SNOMED coded | | `valueString` | Free text | string | Pain description | mild pain | | `valueBoolean` | Yes/no | boolean | Screening flags | true | | `valueInteger` | Whole number | number | Counts | 28 | | `valueRange` | Interval | [Range](/docs/api/fhir/datatypes/range) | Temperature band | 98.0 – 98.7 | | `valueRatio` | Ratio | [Ratio](/docs/api/fhir/datatypes/ratio) | Ratios | example ratio | | `valueSampledData` | Time series | [SampledData](/docs/api/fhir/datatypes/sampleddata) | Wearables summaries | see below | | `valueTime` | Time only | string | Time-of-day phenomena | 15:30:00 | | `valueDateTime` | DateTime | string | Instant readings | 2023-07-24Z | | `valuePeriod` | Interval over time | [Period](/docs/api/fhir/datatypes/period) | Episodes | start–end | #### Using valueSampledData for Time Series `valueSampledData` holds dense series (wearables, CGM summaries) in one `Observation`. Use it when: - Samples repeat on a fixed cadence - A wearable exports many points - Many discrete Observation rows would be inefficient :::warning[Wearables scale] Medplum is not optimized for massive wearable ingestion at second-level granularity across huge populations. Prefer summaries and aggregated Observations in Medplum; stream raw high-frequency feeds to analytics stores when needed. :::
Example: heart rate sampled data ```js { "resourceType": "Observation", "status": "final", "code": { "system": "http://loinc.org", "code": "8867-4", "display": "Heart rate" }, "subject": { "reference": "Patient/example-patient" }, "device": { "reference": "Device/apple-watch-123" }, "effectivePeriod": { "start": "2024-01-15T08:00:00Z", "end": "2024-01-15T09:00:00Z" }, "valueSampledData": { "origin": { "value": 60, "unit": "beats/min", "system": "http://unitsofmeasure.org", "code": "{Beats}/min" }, "period": 60000, "dimensions": 1, "factor": 1, "data": "5 3 2 4 1 2 3 2 1 0 2 1 3 2 4 3 2 1 0 1 2 3 4 5 4 3 2 1 0 1 2 3 4 5 6 5 4 3 2 1 0 1 2 3 4 5 4 3 2 1 0 1 2 3 4 5 4 3 2 1 0" } } ```
##### Helper Functions `@medplum/core` exposes three helpers for working with `valueSampledData`. Use whichever fits the call site–they are independent. `expandSampledData` turns the packed string into an array of numeric values: ```typescript import { expandSampledData } from '@medplum/core'; const values = expandSampledData({ origin: { value: 60, unit: 'beats/min' }, period: 60000, dimensions: 1, factor: 1, data: '5 3 2 4 1', }); ``` `expandSampledObservation` lifts the same expansion to the Observation level, returning one Observation per sample: ```typescript import { expandSampledObservation } from '@medplum/core'; const expandedObservations = expandSampledObservation({ resourceType: 'Observation', code: { /* ... */ }, valueSampledData: { /* ... */ }, effectiveDateTime: '2024-01-15T08:00:00Z', }); ``` `summarizeObservations` and `DataSampler` aggregate many observations into a single summary–useful for dashboards built off wearable data: ```typescript import { summarizeObservations, DataSampler } from '@medplum/core'; const summary = summarizeObservations( observations, { text: 'Average Heart Rate' }, (data) => data.reduce((a, b) => a + b, 0) / data.length, ); const sampler = new DataSampler({ code: { text: 'Heart Rate' }, unit: { unit: 'beats/min', code: '{Beats}/min' }, }); ``` #### Multi-component Observations Use `Observation.component` when multiple values share method, performer, device, and time – classic case is blood pressure. :::caution[Use Sparingly] `component` adds complexity. Prefer it only when necessary. :::
Example: blood pressure panel ```js { "resourceType": "Observation", "id": "example-component-observation", "code": { "system": "http://loinc.org", "code": "85354-9", "display": "Blood pressure panel with all children optional", }, "component": [ { "code": { "system": "http://loinc.org", "code": "8480-6", "display": "Systolic blood pressure", }, "valueQuantity": { "value": 100, "unit": "mmHg", "system": "http://unitsofmeasure.org/", "code": "mm[Hg]", }, }, { "code": { "system": "http://loinc.org", "code": "8462-4", "display": "Diastolic blood pressure", }, "valueQuantity": { "value": 80, "unit": "mmHg", "system": "http://unitsofmeasure.org/", "code": "mm[Hg]", }, }, ], } ```
#### Reference Ranges Use `Observation.referenceRange` for patient-specific normals. Definitions often live in `ObservationDefinition` resources – see [Observation reference ranges](/docs/careplans/reference-ranges). ### Diagnoses and Problem List Diagnoses generally fall into encounter diagnoses versus longitudinal problem list items. [`Condition`](/docs/api/fhir/resources/condition) covers both, plus SDOH and other coded problems. Applications include: - Social determinants of health - Chronic conditions - Substance use - Cognitive or functional impairment #### Condition Elements | Element | Description | Code System | Example | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------- | | `code` | Condition identity | ICD-10, SNOMED | Example code | | `category` | encounter-diagnosis vs problem-list-item | [Condition category](https://build.fhir.org/valueset-condition-category.html) | problem-list-item | | `clinicalStatus` | Active, recurrence, etc. | [Clinical status](https://build.fhir.org/valueset-condition-clinical.html) | active | | `verificationStatus` | Provisional, confirmed, etc. | [Verification](https://build.fhir.org/valueset-condition-ver-status.html) | provisional | | `severity` | Subjective severity | [Severity](https://build.fhir.org/valueset-condition-severity.html) | severe | | `subject` | Patient | | Patient/example | | `onset[x]` | Onset timing | | | | `abatement[x]` | Resolution timing | | | | `recordedDate` | When documented | | | | `stage` | Staging when applicable | | | | `evidence` | Supporting resources such as Observation | | | | `note` | Additional narrative | | | Each `Condition` is an instance of a problem – recurrence is a new resource; link clinically via narrative or extensions as your workflow requires.
Example Condition {ExampleCode}
Example ValueSet of Condition codes {ExampleCode}
Do not use `Condition` for allergies – use [`AllergyIntolerance`](/docs/api/fhir/resources/allergyintolerance); see [Allergies and intolerances](#allergies-and-intolerances). Social factors may still be recorded as `Condition` when that matches your IG. #### Encounter Diagnosis Set `category` to `encounter-diagnosis` and link `encounter` to the visit. Follow [US Core encounter diagnosis](https://hl7.org/fhir/us/core/STU5.0.1/StructureDefinition-us-core-condition-encounter-diagnosis.html) when applicable. #### Problem List Item Use `category` `problem-list-item` for longitudinal active problems. Best practice: keep separate `Condition` instances for encounter diagnoses versus promoted problem-list rows so history stays auditable. Usually require an explicit action to promote an encounter diagnosis to the problem list. #### Symptoms vs Conditions [`Observation`](/docs/api/fhir/resources/observation) is point-in-time; [`Condition`](/docs/api/fhir/resources/condition) is ongoing. Use Observation for transient symptoms; Condition when tracking over time. Example: single fever spike as Observation; prolonged fever course as Condition. More detail appears under [Observations and vital signs](#observations-and-vital-signs). ### Allergies and Intolerances Allergies use [`AllergyIntolerance`](/docs/api/fhir/resources/allergyintolerance). | Element | Description | | -------------------- | ---------------------------------------------------------------- | | `clinicalStatus` | Active vs inactive over time | | `verificationStatus` | Confirmed vs unconfirmed | | `category` | Food, medication, environment, biologic | | `code` | RxNorm for drugs, SNOMED often for foods/environment | | `patient` | Who experiences the allergy | | `encounter` | Encounter where reviewed | | `recorder` | Who documented | | `asserter` | Who asserted (patient, clinician, related person, etc.) | #### Recording Allergy Status Cover: 1. Known allergies – document specifics. 2. No known allergies – document explicit negatives when clinically asserted (for example NKDA concepts). 3. Unknown status – document when workup is required. Only record NKDA or unknown when clinically relevant. Documenting NKDA “by default” when allergies were never assessed can mislead future care. #### Example: Unconfirmed Egg Protein ```js { resourceType: 'AllergyIntolerance', subject: { reference: 'Patient/homer-simpson', }, code: { coding: [ { system: 'http://hl7.org/fhir/sid/snomed', code: '213020009', display: "Allergy to egg protein", }, ], }, verificationStatus: { coding: [ { system: 'http://hl7.org/fhir/ValueSet/condition-ver-status', code: 'unconfirmed', display: 'Unconfirmed', }, ], }, }; ``` #### Example: Patient-Confirmed No Drug Allergies Use SNOMED [no known allergy](https://bioportal.bioontology.org/ontologies/SNOMEDCT?p=classes&conceptid=http%3A%2F%2Fpurl.bioontology.org%2Fontology%2FSNOMEDCT%2F716186003) concepts when appropriate; adjust `asserter` if the source is not the patient. ```js { resourceType: 'AllergyIntolerance', subject: { reference: 'Patient/homer-simpson', }, code: { coding: [ { system: 'http://hl7.org/fhir/sid/snomed', code: '409137002', display: "No known drug allergy", }, ], }, clinicalStatus: { coding: [ { system: 'http://hl7.org/fhir/ValueSet/condition-clinical', code: 'active', display: 'Active', }, ], }, verificationStatus: { coding: [ { system: 'http://hl7.org/fhir/ValueSet/condition-ver-status', code: 'confirmed', display: 'Confirmed', }, ], }, asserter : { reference : 'Patient/homer-simpson', }, }; ``` When allergy documentation is not clinically relevant (for example isolated orthopedic brace care without meds), omit `AllergyIntolerance` rather than forcing NKDA. ### External Documents in the Chart External PDFs and images are indexed with [`DocumentReference`](/docs/api/fhir/resources/documentreference) pointing at [`Binary`](/docs/api/fhir/resources/binary) or URLs – see [External files and Document references](/docs/fhir-datastore/external-documents). ### See Also - [Visit Templates and the SOAP Approach](/docs/charting/visit-templates) - [Intake](/docs/intake/) - [Patient](/docs/api/fhir/resources/patient) FHIR resource API - [Observation](/docs/api/fhir/resources/observation) FHIR resource API - [Condition](/docs/api/fhir/resources/condition) FHIR resource API - [AllergyIntolerance](/docs/api/fhir/resources/allergyintolerance) FHIR resource API - [Parsing Questionnaire Responses](/docs/questionnaires/parsing-questionnaire-responses) - [Ordering Labs And Imaging](/docs/labs-imaging/ordering-labs-imaging) - [Representing Prescriptions](/docs/medications/representing-prescriptions-and-medication-orders) --- ## Designing Charting Charting is one of the most important features of an EMR – and one of the hardest to get right. Clinicians have strong opinions shaped by training, specialty, and years in other systems. The surface experience varies enormously: dense structured forms in some products, narrative-first layouts in others. Underneath all of it, the record still has to support safe care and downstream data use. In Medplum, charting covers the patient summary, encounter documentation, orders, and signing. Medplum is headless: you choose screens, flows, and affordances. The sections below cover the key architecture decisions. ### Why Structure Pays Off with FHIR Every implementation balances structured capture (picklists, measurements, questionnaires) with free narrative (assessment paragraphs, context that does not fit a widget). Leaning structured where the workflow allows is how you get the most from a FHIR backend. When findings, orders, problems, and allergies are first-class resources rather than text blobs, you get: - Trends across visits and population health queries - Search and alerts on real values - Exchange with other systems without bespoke scraping - A clearer path to quality measures and analytics Questionnaires and visit templates are the right lever for structure. Parse submissions into [`Observation`](/docs/api/fhir/resources/observation), [`Condition`](/docs/api/fhir/resources/condition), [`ServiceRequest`](/docs/api/fhir/resources/servicerequest), and related types; narrative still belongs where clinicians need it – for example, assessment prose on [`ClinicalImpression`](/docs/api/fhir/resources/clinicalimpression). ### How Much Structure Is Enough? There is no single answer – urgent care, behavioral health, and procedural specialties impose different rhythms. The tradeoff looks like this: | | More structured | More free text | |---|---|---| | **Strengths** | Queryable, exchangeable, alertable; feeds population health and quality measures | Familiar to clinicians; fast to write; flexible for complex narrative | | **Costs** | Form design + change management overhead; adoption friction | Not queryable as-is; search, handoffs, and quality measures need custom extraction later | Adoption matters as much as architecture. Structure should not erase workflows clinicians have years of muscle memory for. If a section is faster as narrative, force-fitting a form is a step backward. A practical default: structured Subjective, Objective, and Plan paired with narrative Assessment – surfaced alongside a patient summary (problems, allergies, active medications) and inline orders as concrete FHIR requests. :::note Clinical operations teams often maintain a fuller discovery checklist (specialties, encounter modalities, signing policy, template ownership). Use this page as the architectural spine; capture local requirements in whatever artifact your organization already uses. ::: ### See Also - [Visit Templates and the SOAP Approach](/docs/charting/visit-templates) - [Chart Data Model](/docs/charting/chart-data-model) - [Parsing Questionnaire Responses](/docs/questionnaires/parsing-questionnaire-responses) - [Authoring Clinical Protocols](/docs/careplans/protocols) - [Provider Visits](/docs/provider/visits) - [Scheduling](/docs/scheduling/) --- ## Charting :::tip[Planning this workflow?] The [Charting Decision Guide](/docs/decision-guides/charting) walks through requirements questions and FHIR modeling decisions for charting — use it alongside these docs. ::: Charting is where clinicians produce the durable record of care – the patient summary read at every visit, the encounter notes captured during one, and the orders and follow-up that come out of it. Medplum is headless: your app owns the screens, while the underlying FHIR data model keeps the record interoperable, queryable, and reusable. ### Where to Start 1. [Designing Charting](/docs/charting/designing-charting) – discovery questions (visit types, parsing, signing, template ownership). 2. [Visit Templates and the SOAP Approach](/docs/charting/visit-templates) – [`PlanDefinition`](/docs/api/fhir/resources/plandefinition), SOAP mapped to structured S/O/P plus narrative assessment, [`$apply`](/docs/api/fhir/operations/plandefinition-apply), signing. 3. [Chart Data Model](/docs/charting/chart-data-model) – Patient, Observation, Condition, allergies, devices, vitals, and queries for the chart UI. Supporting guides: - [Parsing Questionnaire Responses](/docs/questionnaires/parsing-questionnaire-responses) – `$extract` versus Bots for questionnaire responses. - [Provider Visits](/docs/provider/visits) – Care Templates and Provider app setup (UI-specific). - [Authoring Clinical Protocols](/docs/careplans/protocols) – advanced PlanDefinition composition. ### Sample Application Charting can look however you want. A reference UI built from Medplum [React components](https://storybook.medplum.com/?path=/docs/medplum-introduction--docs) lives in [medplum-provider](https://github.com/medplum/medplum/tree/main/examples/medplum-provider). ![Chart sample](charting-screenshot.png) ### Orders and Billing Orders are usually [`ServiceRequest`](/docs/api/fhir/resources/servicerequest) and [`MedicationRequest`](/docs/api/fhir/resources/medicationrequest) resources, optionally spawned from ActivityDefinitions in visit templates. See [Labs and Imaging](/docs/labs-imaging), [Medications](/docs/medications/representing-prescriptions-and-medication-orders), and [Provider visits](/docs/provider/visits) for billing-oriented ChargeItem flows. ### See Also - [Chart Data Model](/docs/charting/chart-data-model) - [Visit Templates and the SOAP Approach](/docs/charting/visit-templates) - [Questionnaires](/docs/questionnaires/) - [Charting Demo Video](https://youtu.be/PHZr9q20tbM) on YouTube (3 min) - [Sample ICD-9 ValueSet Bundle](https://drive.google.com/file/d/1cFHGBud9IlGH86yilxe-KkDxGUbGr2Mn/view?usp=sharing) - [Charting Features and Fixes](https://github.com/medplum/medplum/pulls?q=is%3Apr+label%3Acharting) on GitHub --- ## Visit Templates and the SOAP Approach Most clinical products still organize encounter documentation around some variant of SOAP – Subjective, Objective, Assessment, Plan – even when the screen does not label the sections that way. Visit templates are how Medplum ties that workflow to FHIR: instead of a single undifferentiated note, you instantiate a [`PlanDefinition`](/docs/api/fhir/resources/plandefinition) per visit type so clinicians get the right questionnaires and order tasks when the encounter opens (Care Templates in Medplum Provider are this concept in the UI). The pattern this guide recommends maps SOAP to FHIR with one deliberate split: Subjective, Objective, and Plan are primarily structured resources (Observations from forms or devices; ServiceRequest / MedicationRequest / CarePlan for orders), while Assessment is where narrative free text belongs – on [`ClinicalImpression`](/docs/api/fhir/resources/clinicalimpression), with coded diagnoses on [`Condition`](/docs/api/fhir/resources/condition) when you need them. That keeps the record interoperable and searchable without asking clinicians to avoid prose where judgment is the point. See [Designing Charting](/docs/charting/designing-charting) for product framing. For authoring `PlanDefinition` graphs, nested actions, and ActivityDefinitions, see [Authoring Clinical Protocols](/docs/careplans/protocols). For configuring Care Templates in the hosted app, see [Provider Visits](/docs/provider/visits). ### Visit Template Lifecycle 1. Author a `PlanDefinition` whose `action` entries reference Questionnaires and ActivityDefinitions. Authoring detail lives in [Authoring Clinical Protocols](/docs/careplans/protocols). 2. Invoke [`$apply`](/docs/api/fhir/operations/plandefinition-apply) with `subject` (Patient) and optionally `encounter`, `practitioner`, and `organization`. 3. Medplum creates Tasks (and ServiceRequests when `ActivityDefinition.kind` is ServiceRequest). 4. Clinicians complete Questionnaires and orders; parse responses into FHIR resources using [Parsing Questionnaire Responses](/docs/questionnaires/parsing-questionnaire-responses) or Bots. 5. Sign the visit using `ClinicalImpression.status` and a `Provenance` on the Encounter (see [Signing and Locking Notes](#signing-and-locking-notes)). ### Common Visit-Template Patterns A few shapes recur in real implementations. - _Questionnaire-driven capture (primary care)_ – a standard form per visit type (vitals, intake screen) submits a `QuestionnaireResponse` whose answers are parsed into `Observation` resources via `$extract` or a Bot. - _Template-driven plan (specialty protocols)_ – a `PlanDefinition` for the protocol instantiates `ServiceRequest`, `MedicationRequest`, or `CarePlan` from `ActivityDefinition` actions on `$apply` rather than free-text plan sections. See [Authoring Clinical Protocols](/docs/careplans/protocols). - _Pre-filled context from intake_ – when intake narrows the problem list (e.g. dermatology lesions), prepopulate Questionnaire or Observation drafts before the clinician opens the chart. Avoid storing everything as raw `QuestionnaireResponse` only – answers there are not first-class searchable fields the way Observations and Conditions are. Parse into the proper resources (see [Parsing Questionnaire Responses](/docs/questionnaires/parsing-questionnaire-responses)). ### How Visits, FHIR, and SOAP Line Up The diagram below is intentionally simplified – real encounters interleave steps – but shows how common visit steps map to FHIR resources and SOAP letters. Template mechanics (what `$apply` creates first) are separate from documentation; everything eventually references the same Encounter. ```mermaid flowchart LR subgraph visit_steps["Visit steps"] direction TB vs1[Patient-reported findings and forms] vs2[Vitals and measured exam data] vs3[Assessment reasoning and diagnoses] vs4[Orders referrals meds follow-up] end subgraph fhir_art["FHIR resources"] direction TB fh1[Observations from questionnaires Tasks and parsing] fh2[Observations vitals and exam] fh3[ClinicalImpression and Condition] fh4[ServiceRequest MedicationRequest CarePlan] end subgraph soap_map["SOAP"] direction TB sp1[S] sp2[O] sp3[A] sp4[P] end vs1 --> fh1 --> sp1 vs2 --> fh2 --> sp2 vs3 --> fh3 --> sp3 vs4 --> fh4 --> sp4 ``` The encounter graph below shows the same resources hanging off a single [`Encounter`](/docs/api/fhir/resources/encounter): ```mermaid flowchart BT patient[Patient] encounter[Encounter] obsSubjective["Observation (Subjective)"] obsObjective["Observation (Objective)"] clinicalImpression["ClinicalImpression (Assessment)"] condition["Condition (Diagnosis)"] serviceRequest["ServiceRequest / MedicationRequest (Plan)"] encounter -->|subject| patient obsSubjective -->|encounter| encounter obsObjective -->|encounter| encounter clinicalImpression -->|encounter| encounter condition -->|encounter| encounter serviceRequest -->|encounter| encounter clinicalImpression -.->|"leads to"| condition ```
Example: full SOAP note FHIR R4 Bundle [Download soap-note-bundle.json](/examples/soap-note-bundle.json) ```json { "resourceType": "Bundle", "type": "collection", "entry": [ { "fullUrl": "urn:uuid:example-encounter", "resource": { "resourceType": "Encounter", "id": "example-encounter", "status": "finished", "class": { "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", "code": "AMB", "display": "ambulatory" }, "subject": { "reference": "Patient/homer-simpson" }, "participant": [ { "individual": { "reference": "Practitioner/dr-alice-smith" } } ], "period": { "start": "2024-01-15T10:00:00Z", "end": "2024-01-15T11:30:00Z" } } }, { "fullUrl": "urn:uuid:obs-subjective-fatigue", "resource": { "resourceType": "Observation", "id": "obs-subjective-fatigue", "status": "final", "code": { "coding": [ { "system": "http://loinc.org", "code": "75325-1", "display": "Symptom" } ], "text": "Fatigue" }, "subject": { "reference": "Patient/homer-simpson" }, "encounter": { "reference": "Encounter/example-encounter" }, "performer": [{ "reference": "Patient/homer-simpson" }], "valueString": "Patient reports feeling lethargic for the past week" } }, { "fullUrl": "urn:uuid:obs-objective-heart-rate", "resource": { "resourceType": "Observation", "id": "obs-objective-heart-rate", "status": "final", "code": { "coding": [ { "system": "http://loinc.org", "code": "8867-4", "display": "Heart rate" } ] }, "subject": { "reference": "Patient/homer-simpson" }, "encounter": { "reference": "Encounter/example-encounter" }, "performer": [{ "reference": "Practitioner/dr-alice-smith" }], "valueQuantity": { "value": 112, "unit": "beats/min", "system": "http://unitsofmeasure.org", "code": "{Beats}/min" } } }, { "fullUrl": "urn:uuid:clinical-impression-assessment", "resource": { "resourceType": "ClinicalImpression", "id": "clinical-impression-assessment", "status": "completed", "subject": { "reference": "Patient/homer-simpson" }, "encounter": { "reference": "Encounter/example-encounter" }, "date": "2024-01-15T11:00:00Z", "description": "Patient presents with fatigue and abdominal pain.", "finding": [ { "itemReference": { "reference": "Condition/condition-gastritis" } } ], "note": [ { "text": "Assessment: symptoms consistent with gastritis. Differential includes peptic ulcer disease. Will monitor response to treatment." } ] } }, { "fullUrl": "urn:uuid:condition-gastritis", "resource": { "resourceType": "Condition", "id": "condition-gastritis", "clinicalStatus": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", "code": "active" } ] }, "verificationStatus": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/condition-ver-status", "code": "confirmed" } ] }, "code": { "coding": [ { "system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "K29.70", "display": "Gastritis, unspecified, without bleeding" } ] }, "subject": { "reference": "Patient/homer-simpson" }, "encounter": { "reference": "Encounter/example-encounter" } } }, { "fullUrl": "urn:uuid:service-request-lab", "resource": { "resourceType": "ServiceRequest", "id": "service-request-lab", "status": "active", "intent": "order", "code": { "coding": [ { "system": "http://loinc.org", "code": "13958-0", "display": "Helicobacter pylori [Presence] in Stool by Immunoassay" } ] }, "subject": { "reference": "Patient/homer-simpson" }, "encounter": { "reference": "Encounter/example-encounter" }, "requester": { "reference": "Practitioner/dr-alice-smith" } } }, { "fullUrl": "urn:uuid:provenance-note-signed", "resource": { "resourceType": "Provenance", "id": "provenance-note-signed", "target": [{ "reference": "Encounter/example-encounter" }], "recorded": "2024-01-15T11:30:00Z", "reason": [ { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/v3-ActReason", "code": "SIGN", "display": "Signed" } ] } ], "agent": [ { "type": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/provenance-participant-type", "code": "author" } ] }, "who": { "reference": "Practitioner/dr-alice-smith" } } ] } } ] } ```
### Subjective and Objective – Observation Both Subjective and Objective are [`Observation`](/docs/api/fhir/resources/observation) resources. The distinction is usually `performer`: - Subjective: patient as `performer` for self-reported symptoms. - Objective: clinician or device as `performer` for measured findings. Use appropriate coding (typically [LOINC](/docs/careplans/loinc)) and `value[x]`. For measurement details see [Observations and vital signs](/docs/charting/chart-data-model#observations-and-vital-signs).
Example: patient-reported fatigue (Subjective) ```json { "resourceType": "Observation", "status": "final", "code": { "coding": [ { "system": "http://loinc.org", "code": "75325-1", "display": "Symptom" } ], "text": "Fatigue" }, "subject": { "reference": "Patient/homer-simpson" }, "encounter": { "reference": "Encounter/example-encounter" }, "performer": [{ "reference": "Patient/homer-simpson" }], "valueString": "Patient reports feeling lethargic for the past week" } ```
Example: elevated heart rate (Objective) ```json { "resourceType": "Observation", "status": "final", "code": { "coding": [ { "system": "http://loinc.org", "code": "8867-4", "display": "Heart rate" } ] }, "subject": { "reference": "Patient/homer-simpson" }, "encounter": { "reference": "Encounter/example-encounter" }, "performer": [{ "reference": "Practitioner/dr-alice-smith" }], "valueQuantity": { "value": 112, "unit": "beats/min", "system": "http://unitsofmeasure.org", "code": "{Beats}/min" } } ```
### Assessment – Clinical Impression [`ClinicalImpression`](/docs/api/fhir/resources/clinicalimpression) is the FHIR-native assessment resource – the “A” in SOAP. :::tip[Why ClinicalImpression instead of DocumentReference or raw QuestionnaireResponse?] Some apps store assessment as `DocumentReference` or leave narrative only in `QuestionnaireResponse`. `ClinicalImpression` gives findings, summary, and reasoning discrete fields other systems can consume. Prefer structured coded data and use free-text where clinicians need it. ::: Usually create `ClinicalImpression` early with `status` `in-progress`, then transition to `completed` when signed. | Field | Description | Example | | ------------- | ----------------------------------------------- | ------------------------------------------------------- | | `status` | Lifecycle state | `in-progress`, `completed` | | `subject` | Patient | `Patient/homer-simpson` | | `encounter` | Encounter | `Encounter/example-encounter` | | `date` | When assessed | `2024-01-15T10:00:00Z` | | `description` | Short summary | Patient presents with fatigue and abdominal pain. | | `note` | Narrative; `note[0].text` is common for the note | Assessment narrative text |
Example: ClinicalImpression at encounter start ```json { "resourceType": "ClinicalImpression", "status": "in-progress", "subject": { "reference": "Patient/homer-simpson" }, "encounter": { "reference": "Encounter/example-encounter" }, "date": "2024-01-15T10:00:00Z", "description": "Patient presents with fatigue and abdominal pain.", "note": [ { "text": "Assessment: symptoms consistent with gastritis. Differential includes peptic ulcer disease. Will monitor response to treatment." } ] } ```
Formal diagnoses are often modeled as [`Condition`](/docs/api/fhir/resources/condition) while `ClinicalImpression` carries reasoning. See [Diagnoses and problem list](/docs/charting/chart-data-model#diagnoses-and-problem-list). ### Plan – Orders and Care Plan items are concrete FHIR requests: [`ServiceRequest`](/docs/api/fhir/resources/servicerequest) for labs, imaging, and referrals (use `category` to distinguish), [`MedicationRequest`](/docs/api/fhir/resources/medicationrequest) for prescriptions, and [`CarePlan`](/docs/api/fhir/resources/careplan) for ongoing care strategy. Generate them inline from `ActivityDefinition` actions on the visit template rather than free text. See [Ordering Labs and Imaging](/docs/labs-imaging/ordering-labs-imaging) and [Representing Prescriptions](/docs/medications/representing-prescriptions-and-medication-orders) for detail. ### Signing and Locking Notes When the note is complete, set `ClinicalImpression.status` to `completed` and create [`Provenance`](/docs/api/fhir/resources/provenance) targeting the Encounter to record signer and time. "Signed" and "locked" are separate product decisions – co-signing, post-sign editable windows, and amendment workflows belong in your own discovery; see [Designing Charting](/docs/charting/designing-charting) for the structure-vs-narrative framing.
Example: Provenance for clinician sign-off ```json { "resourceType": "Provenance", "target": [{ "reference": "Encounter/example-encounter" }], "recorded": "2024-01-15T11:30:00Z", "reason": [ { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/v3-ActReason", "code": "SIGN", "display": "Signed" } ] } ], "agent": [ { "type": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/provenance-participant-type", "code": "author" } ] }, "who": { "reference": "Practitioner/dr-alice-smith" } } ] } ```
### See Also - [Authoring Clinical Protocols](/docs/careplans/protocols) - [ClinicalImpression](/docs/api/fhir/resources/clinicalimpression) FHIR resource API - [Observation](/docs/api/fhir/resources/observation) FHIR resource API - [Condition](/docs/api/fhir/resources/condition) FHIR resource API - [Chart Data Model](/docs/charting/chart-data-model) - [PlanDefinition `$apply`](/docs/api/fhir/operations/plandefinition-apply) - [medplum-provider example app](https://github.com/medplum/medplum/tree/main/examples/medplum-provider) --- ## Connecting to External FHIR Servers When building an application, you many need to query or write data to an external FHIR server as part of your application's workflow. For example: - When a new patient is created, see if that patient already exists in another server - When an encounter is finalized, synchronize the encounter documentation to another FHIR server - On a bi-weekly basis, pull new FHIR data from a partner using a Bulk FHIR API To enable these scenarios, you will need a `clientId` or `clientSecret` to access the system you want to connect to. Please note that different systems have different levels of functionality, and so the commands in the CLI are not guaranteed to work. The examples below use the [CLI optional flags](/docs/cli#optional-flags-for-medplum-commands). ### Setting up your credentials Medplum CLI stores credentials to be used in a future period without needing it to be entered in every command. By using the `profile` command, this helps with the ability to work with multiple FHIR servers. #### Setting a Profile In this example, we will set up a profile using `medplum profile set ` with the flags below: - --auth-type - --client-id - --client-secret - --base-url - --token-url - --authorize-url - --fhir-url-path #### Syntax ```bash medplum profile set \ --auth-type \ --base-url \ --fhir-url-path \ --token-url \ --client-id \ --client-secret ``` | Accepted Auth Type | | ------------------ | | basic | | client-credentials | | authorization-code | | jwt-bearer | The profile will now be stored in a file directory in `~.medplum/.json` Once you have a profile, you can connect with external FHIR servers with your profile using the `-p` flag. #### Example: Basic Auth ```bash medplum profile set example \ --auth-type "basic" \ --base-url "https://api.example.com" \ --fhir-url-path "fhir/R4" \ --client-id "MY_CLIENT_ID" \ --client-secret "MY_CLIENT_SECRET" ``` #### Example: JWT Bearer ```bash medplum profile set example \ --auth-type "jwt-bearer" \ --base-url "https://api.example.com" \ --fhir-url-path "fhir/R4" \ --token-url "/oauth2/token" \ --client-id "MY_CLIENT_ID" \ --client-secret "MY_CLIENT_SECRET" --scope "openid profile" --audience "/oauth2/token" --subject "john_doe" --issuer "api.example.com" ``` #### Example: JWT Assertion ```bash medplum profile set example \ --auth-type "jwt-assertion" \ --base-url "https://api.example.com" \ --fhir-url-path "fhir/R4" \ --token-url "/oauth2/token" \ --client-id "MY_CLIENT_ID" \ --audience "/oauth2/token" \ --subject "john_doe" \ --private-key-path "/path/to/privatekey.pem" ``` :::note[] If you are authenticating with Epic and it returns `\{"error":"invalid_client","error_description":null\}`, here is a [Doc to Troubleshoot](https://fhir.epic.com/Resources/jwt_auth_troubleshoot_eof). ::: #### Example: Client Credentials ```bash medplum profile set example \ --auth-type "client-credentials" \ --base-url "https://api.example.com" \ --fhir-url-path "fhir/R4" \ --token-url "oauth2/token" \ --client-id "MY_CLIENT_ID" \ --client-secret "MY_CLIENT_SECRET" ``` #### Example: User Login / Authorization Code ```bash medplum profile set example \ --base-url "https://api.example.com" \ --fhir-url-path "fhir/R4" \ --authorize-url "oauth2/authorize" \ --token-url "oauth2/token" ``` Other profile commands include: ##### `describe` To see the state of your credentials in on profile ###### Syntax ```bash medplum profile describe example ``` ###### Example ```bash medplum profile describe ``` ##### `remove` Removing a profile ###### Syntax ```bash medplum profile remove ``` ###### Example ```bash medplum profile remove example ``` ##### `list` To see all of your profiles ```bash medplum profile list ``` After your profiles are set, you can login with your credentials and use them on future commands. To login: `medplum login --profile ` For more information take a look at our [CLI login](/docs/cli#auth) ### Logging in with a new Profile You can run `medplum login --profile ` and set the same flags that [profile](/docs/cli/external-fhir-servers#setting-a-profile) has to set a profile and automatically login #### Example: Logging in with JWT Bearer ```bash medplum login \ --profile "example" \ --auth-type "jwt-bearer" \ --base-url "https://api.example.com" \ --fhir-url-path "fhir/R4" \ --token-url "/oauth2/token" \ --client-id "MY_CLIENT_ID" \ --client-secret "MY_CLIENT_SECRET" --scope "openid profile" --audience "/oauth2/token" --subject "john_doe" --issuer "api.example.com" ``` :::note[] When running `medplum login --profile `, all of the flags need to be set with the flags and valid data ::: ### Example: Basic search In this example, we will show how to search for a patient by identifier using the command line after a profile has been set. ```bash medplum get -p 'Patient?identifier:contains=3SH0A00AA00' ``` ### Example: Create encounter In this example, an Encounter is created in another system using the command line after a profile has been set. ```bash medplum post -p Encounter '{"resourceType": "Encounter", "status": "finished", "class": {"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", "code": "AMB"}, "type": [{"coding": [{"system": "http://snomed.info/sct", "code": "162673000", "display": "General examination of patient (procedure)"}], "text": "General examination of patient (procedure)"}], "subject": {"reference": "Patient/13e44a47-636b-49e2-adb3-9f19c7e0e47a", "display": "Mr. Dustin31 Ritchie586"}}' ``` ### Example: Bulk FHIR Export In this example, Bulk FHIR ndjson files are exported from the server and stored on on the local drive after a profile has been set. ```bash medplum bulk export -p -e Group/all ``` For example, [CMS BCDA](https://bcda.cms.gov/) publishes a Bulk FHIR test server. You can store [test credentials here](https://bcda.cms.gov/guide.html#try-the-api) in a profile and try the following command. ```bash medplum profile set bcda-sandbox --base-url https://sandbox.bcda.cms.gov --fhir-url-path api/v2/ --token-url https://sandbox.bcda.cms.gov/auth/token --client-id --client-secret ``` And then run ```bash medplum bulk export -p bcda-sandbox -e Group/all ``` ### Next Steps The Medplum CLI uses Medplum [TypescriptSDK](/docs/sdk) to power the functionality. Once the external connection is working and you have tested some of the basic scenarios, it is recommended to build out your integration as a [bot](/docs/bots) to enable your event driven or [cron-based](/docs/bots/bot-cron-job) workflow. ### Related Resources - [Epic FHIR Test Environment](https://fhir.epic.com/Developer/Apps) - [eClinicalworks FHIR documentation](https://fhir.eclinicalworks.com/ecwopendev/documentation#) - [Cerner FHIR Documentation](https://fhir.cerner.com/millennium/r4/#open-sandbox) - [Health Gorilla FHIR Documentation](https://developer.healthgorilla.com/docs/oauth20) --- ## Intro The Medplum CLI (Command Line Interface) is a set of command line tools to quickly deploy Medplum web applications and Medplum bots. ### Prerequisites The Medplum CLI requires [Node.js](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) version 22+ ### Installation Add globally: ```bash npm install --global @medplum/cli ``` Or add as a package dependency: ```bash npm install @medplum/cli ``` ### Authentication Use one of these authentication options: 1. Stored credentials in `~/.medplum/credentials`. You can use the `medplum login` command (see below) to automatically create this file. 2. Client credentials in environment variables `MEDPLUM_CLIENT_ID` and `MEDPLUM_CLIENT_SECRET`. `dotenv` is enabled, so you can store them in a `.env` file. 3. Client credentials in optional command flags. - `--client-id ` - FHIR server client id - `--client-secret ` - FHIR server client secret ### Usage If installed globally, you can use the `medplum` command directly: ```bash medplum ``` If installed as a package dependency, you can use the `medplum` command via `npx`: ```bash npx medplum ``` By default, the `medplum` command uses the Medplum hosted API at "https://api.medplum.com". If you want to use the `medplum` command against your own self-hosted server, you can use the `MEDPLUM_BASE_URL` environment variable. `dotenv` is enabled, so you can store this value in a `.env` file. #### optional flags for medplum commands - `--base-url ` - FHIR server base url - `--fhir-url-path ` - FHIR server url path - `--tokenUrl ` - FHIR server token url - `--authorizeUrl ` - FHIR server authorize url ```bash medplum get --base-url https://api.example.com 'Patient/homer-simpson' ``` #### Auth ##### `login` The `login` command opens a web browser to a Medplum authentication page. On successful login, the command writes credentials to disk at `~/.medplum/credentials`. The `medplum` command will then load those credentials on all future runs. Example: ```bash medplum login ``` ##### `whoami` The `whoami` command displays whether the client is authenticated, and, if so, the name of the current user and current Medplum project. ```bash medplum whoami ``` #### RESTful Operations The `medplum` command can be used as a convenient tool for basic Medplum CRUD and RESTful operations. While all API endpoints are available to any command line HTTP client such as `curl` or `wget`, there are a few advantages to using the `medplum` command: 1. Authentication and credentials - login once using the `login` command, and the `Authorization` header will be set automatically. 2. URL prefixes - adds the base URL (i.e., "https://api.medplum.com") and FHIR path prefix (i.e., "fhir/R4/"). 3. Pretty print - formats the JSON with spaces and newlines. 4. Medplum extended mode - adds the `X-Medplum` HTTP header for private Medplum fields. ##### `get` Makes an HTTP `GET` request. ```bash medplum get ``` Example: Search for patients: ```bash medplum get 'Patient?name=homer' ``` Example: Read patient by ID: ```bash medplum get Patient/$id ``` ##### flags - `--as-transaction` - Convert the output response to a transaction bundle ##### `post` Makes an HTTP `POST` request. ```bash medplum post ``` Example: Create a patient: ```bash medplum post Patient '{"resourceType":"Patient","name":[{"family":"Simpson"}]}' ``` Example: Invoke a FHIR operation: ```bash medplum post 'Patient/$validate' '{"resourceType":"Patient","name":[{"family":"Simpson"}]}' ``` ##### `put` Makes an HTTP `PUT` request. ```bash medplum put ``` Example: Update a patient: ```bash medplum put Patient/$id '{"resourceType":"Patient","name":[{"family":"Simpson"}]}' ``` ##### `patch` Makes an HTTP `PATCH` request. ```bash medplum patch ``` Example: Update a patient with [JSONPatch](https://jsonpatch.com/): ```bash medplum patch Patient/$id '[{"op":"add","path":"/active","value":[true]}]' ``` ##### `delete` Makes an HTTP `DELETE` request. ```bash medplum delete ``` Example: Delete patient by ID: ```bash medplum delete Patient/$id ``` #### Project `project` will have administration commands for visibility and management ##### `current` Sees your current project ```bash medplum project current ``` ##### `list` Sees your list of project ids and names ```bash medplum project list ``` ##### `switch` Switching to another project from the current one ```bash medplum project switch ``` ##### `invite` Invite a user to a project ```bash medplum project invite ``` ##### optional flags for `invite` - `--send-email` - If you want to send the email when inviting the user - `--admin` - If the user you are inviting is an admin - `--role` `-r` `` - The type of role the new user will have - Choices are Patient, Practitioner, and RelatedPerson - Default will be Practitioner #### AWS `aws` includes commands for creating and managing AWS resources. The AWS commands require [AWS authentication and access credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-authentication.html). Please make sure your credentials are configured before using the `aws` commands. :::caution[] The `aws` commands are in beta, and likely to change. ::: ##### list List your Medplum deployments. This command lists AWS CloudFormation stacks with the `medplum:environment` tag. ```bash medplum aws list ``` ##### describe Describe a Medplum deployment. Displays select AWS resources such as ECS Cluster, ECS Service, and S3 buckets from the AWS CloudFormation stack with the corresponding `medplum:environment` tag. ```bash medplum aws describe ``` ##### update-app Updates the app S3 bucket in a Medplum deployment to the latest version. ```bash medplum aws update-app ``` ##### update-server Updates the ECS Service in a Medplum deployment to the latest version. ```bash medplum aws update-server ``` #### Bots ##### Bots Config file Create a Medplum config file called `medplum.config.json`: ```json { "bots": [ { "name": "hello-world", "id": "f0465c2e-11d4-4c36-b834-8e86f7472b4b", "source": "src/index.ts", "dist": "dist/index.js" } ] } ``` The `name` property is a friendly name you can use to reference the Bot in commands. The `id` property refers to the Bot ID in your Medplum project. The `source` property is the file path to the original source. When you "save" the Bot, the contents of this file will be saved to the Bot `code` property. This file can be JavaScript or TypeScript. The `dist` property is the optional file path to the compiled source. If omitted, the command falls back to using the `source` property. When you "deploy" the Bot, the contents of this file will be deployed to the Bot runtime. This file must be JavaScript. ##### bot save Updates the `code` value on a `Bot` resource Syntax: ```bash npx medplum bot save ``` Example: ```bash npx medplum bot save hello-world ``` ##### bot deploy Deploys the Bot code Syntax: ```bash npx medplum bot deploy ``` Example: ```bash npx medplum bot deploy hello-world ``` ##### bot create Creates a bot and saves it Syntax: ```bash npx medplum bot create ``` ##### Bots Example Create a Medplum config file `medplum.config.json`: ```json { "bots": [ { "name": "hello-world", "id": "f0465c2e-11d4-4c36-b834-8e86f7472b4b", "source": "src/hello-world.ts", "dist": "dist/hello-world.js" } ] } ``` Replace the sample `id` with your Bot's ID. Write your bot in `src/hello-world.ts`. This can be TypeScript. It can reference `@medplum/core` and `node-fetch`: ```ts import { MedplumClient } from '@medplum/core'; import { Resource } from '@medplum/fhirtypes'; export async function handler(medplum: MedplumClient, event: BotEvent): Promise { console.log('Hello world'); } ``` You can use the Medplum CLI to save it: ```bash npx medplum bot save hello-world ``` Compile with vanilla `tsc` (no bundler required) ```bash npx tsc ``` The result will be JavaScript output in `dist/hello-world.js`: ```javascript export async function handler(medplum, input) { console.log('Hello world'); } ``` You can then use the Medplum CLI to deploy it. ```bash npx medplum bot deploy hello-world ``` #### Bulk `bulk` includes commands for requesting bulk data export from a FHIR resource server. `bulk` also includes the functionality to import ndjson files generated by bulk data exports from other systems. ##### export `bulk export` handles the request flows documented at https://build.fhir.org/ig/HL7/bulk-data/export.html and downloads all attachment files. ```bash medplum bulk export [options] ``` ###### optional flags for `bulk export` - `-e, --export-level ` - Export level. Defaults to system level export. - "Group/:id" - Group of Patients - "Patient" - All Patients. - `-t, --types ` - Resource types to export - `-s, --since ` - Resources will be included in the response if their state has changed after the supplied time (e.g. if Resource.meta.lastUpdated is later than the supplied \_since time). - `-d, --target-directory ` - Target directory to save files from the bulk export operations. ##### import `bulk import` imports ndjson files generated by bulk data exports. ```bash medplum bulk import [options] ``` ###### optional flags for `bulk import` - `--num-resources-per-request ` - number of resources to import per batch request. Defaults to 25. - `-d, --target-directory ` - Target directory of file to be imported. #### Generate Types `generate-types` generates TypeScript definitions from FHIR StructureDefinitions, such as your own profiles. Input files can contain a single StructureDefinition, an array, or a Bundle, and StructureDefinitions must include a `snapshot`. See [TypeScript Types for Profiles](/docs/fhir-datastore/profiles#typescript-types-for-profiles) for details. ```bash medplum generate-types [options] [files...] ``` For example: ```bash medplum generate-types us-core-profiles.json --output-dir src/types/us-core ``` ```bash medplum generate-types --profile-url https://example.com/profiles/my-patient/1.0.0 --output-dir src/types/profiles ``` ###### optional flags for `generate-types` - `--profile-url ` - Canonical URL of a StructureDefinition to fetch from your Medplum server. Can be repeated to fetch multiple profiles. Uses the same authentication options as other commands. - `-o, --output-dir ` - Output directory for the generated `.d.ts` files. Defaults to the current directory. --- ## Representing Asynchronous Encounters import ExampleCode from '!!raw-loader!@site/../examples/src/communications/messaging-examples.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ### Intro In healthcare, an "encounter" refers to any diagnostic or treatment interaction between a patient and provider. In traditional healthcare settings, this typically refers to an in-person visit, and is represented by the [`Encounter`](/docs/api/fhir/resources/encounter) resource. But in digital health, this can take on a variety of asynchronous forms, including SMS chains, in-app chat threads, or even an email exchange. In this guide, we'll show you how to represent these kinds of asynchronous encounters in FHIR. ### Defining Sessions To get started, you'll first need to determine what determines a "session" in your care setting. A session could be a single SMS chain, or a single email thread. Many digital health apps ask the patient to explicitly initiate a messaging session with a provider. Alternatively, if your care setting has more of a rolling interaction model (e.g. a continuous text thread), you may choose to group all communications from the same day into a session. Other common choices include treating one messaging thread as one session, or starting a session only when the patient explicitly opens a new care interaction. ### Representing Sessions in FHIR Each session should be represented by an [`Encounter`](/docs/api/fhir/resources/encounter) resource. All of the messages that are part of this session should be represented as a thread of [`Communication`](/docs/api/fhir/resources/communication) resources. The thread should be linked to the session using the [`Communication.encounter`](/docs/api/fhir/resources/communication) element of only the thread header. For more details on modeling threads, see [Building and Structuring Threads](/docs/communications/messaging-data-model#building-and-structuring-threads). ```mermaid flowchart RL A(Encounter) subgraph Thread Header B(Communication) end subgraph Messages C(Communication) D(Communication) end C --> |Communication.partOf| B D --> |Communication.partOf| B B --> |Communication.encounter| A ``` :::note[Encounter on the thread header only] Set [`Communication.encounter`](/docs/api/fhir/resources/communication) on the thread header only. Child messages point at the header with [`Communication.partOf`](/docs/api/fhir/resources/communication); they do not need their own `encounter` element for this pattern. ::: You should record the participating physicians using the [`Encounter.participant`](/docs/api/fhir/resources/encounter) element. You can also record any family members who are part of the session here (see our [Family Relationships guide](/docs/fhir-datastore/family-relationships)). :::tip[Asynchronous Encounter Ontologies] The [`Encounter.class`](/docs/api/fhir/resources/encounter) element is required in FHIR and should be taken from the [HL7 Act Encounter Code Valueset](https://terminology.hl7.org/3.1.0/ValueSet-v3-ActEncounterCode.html). Asynchronous care contexts will almost always use the code `VR` ("virtual"). Also check our [USCDI guide](/docs/fhir-datastore/understanding-uscdi-dataclasses) for information on how to make your [`Encounter`](/docs/api/fhir/resources/encounter) compatible with the US Core standards. ::: If your session only involves providing care for a single patient, then you can set the [`Encounter.subject`](/docs/api/fhir/resources/encounter) element to refer to the patient and you're all set! If, however, multiple patients are involved in the session, continue reading. ### Walkthrough: Session Encounter and Thread Link If you need encounters for billing, quality programs, or compliance, create a session [`Encounter`](/docs/api/fhir/resources/encounter), then link your existing thread header with a JSON Patch on [`Communication.encounter`](/docs/api/fhir/resources/communication). This assumes you already have a thread header [`Communication`](/docs/api/fhir/resources/communication). For how thread headers and messages are structured, see [Building and Structuring Threads](/docs/communications/messaging-data-model#building-and-structuring-threads). #### Create the Session Encounter The snippet uses virtual class `VR` and example [`Patient`](/docs/api/fhir/resources/patient) and [`Practitioner`](/docs/api/fhir/resources/practitioner) references. Replace them with ids that exist in your project. {ExampleCode} #### Link the Thread Header to the Encounter Patch only the thread header. The snippet uses the session Encounter id from the previous step and an existing thread header [`Communication`](/docs/api/fhir/resources/communication) (no `payload`, no `partOf`). {ExampleCode} ### Handling Multiple-Patient Sessions In some care settings, a session may discuss the health of multiple patients. For example, a mother may ask about the health of both of her children in the same email exchange. In these situations, we'll have to represent distinct "medical encounters" for each patient. We can use the hierarchical nature of the [`Encounter`](/docs/api/fhir/resources/encounter) resource to split out medical encounters for each session. The [`Encounter.partOf`](/docs/api/fhir/resources/encounter) element creates a parent-child relationship between [`Encounters`](/docs/api/fhir/resources/encounter), which is perfect for encounters that overlap in time. To properly represent your asynchronous encounter, you should: 1. Create an [`Encounter`](/docs/api/fhir/resources/encounter) for the session 2. Create a new [`Encounter`](/docs/api/fhir/resources/encounter) for each patient to represent a medical encounter 3. Set the [`Encounter.subject`](/docs/api/fhir/resources/encounter) of each medical encounter to the corresponding patient 4. Populate each medical encounter with the clinical details (diagnoses, reasons for visit) of the corresponding patient. 5. Set the [`Encounter.partOf`](/docs/api/fhir/resources/encounter) element for each medical encounter to refer to the session's [`Encounter`](/docs/api/fhir/resources/encounter) The thread of [`Communication`](/docs/api/fhir/resources/communication) resources should still be linked to the "session" encounter, but the clinical details for each patient will live on the medical encounters. You can add a value to the [`Encounter.type`](/docs/api/fhir/resources/encounter) element to tag encounters as either "sessions" or "medical encounters." ```mermaid flowchart TD SessionEncounter[Session Encounter] MedEncA[Medical encounter subject Patient A] MedEncB[Medical encounter subject Patient B] ThreadHeader[Thread header Communication] Msg1[Child message] Msg2[Child message] MedEncA -->|Encounter.partOf| SessionEncounter MedEncB -->|Encounter.partOf| SessionEncounter ThreadHeader -->|Communication.encounter| SessionEncounter Msg1 -->|Communication.partOf| ThreadHeader Msg2 -->|Communication.partOf| ThreadHeader ``` While creating an [`Encounter`](/docs/api/fhir/resources/encounter) hierarchy like this is a bit more work up front, it promotes good data hygiene. A well documented encounter, with the correct practitioner, diagnosis codes, [`Encounter.serviceType`](/docs/api/fhir/resources/encounter), and [`Encounter.reasonCode`](/docs/api/fhir/resources/encounter) per patient is critical to [billing](/docs/billing), and it is important for patient analytics use cases, such as computing quality of care metrics. #### Walkthrough: Child Encounter for One Patient Create one child [`Encounter`](/docs/api/fhir/resources/encounter) per patient, referencing the session Encounter from the walkthrough above in [`Encounter.partOf`](/docs/api/fhir/resources/encounter). Use [`Encounter.type`](/docs/api/fhir/resources/encounter) (here a custom coding) to distinguish medical encounters from the parent session. Replace the patient reference with your data. {ExampleCode} :::tip[Family participants] Record family members involved in the session on [`Encounter.participant`](/docs/api/fhir/resources/encounter). See [Family Relationships](/docs/fhir-datastore/family-relationships) for modeling related persons. ::: ### Verify with Search Use these searches to confirm virtual encounters exist and that the thread header is linked to the session Encounter. The TypeScript tab chains the session Encounter id from the walkthrough above. For CLI and cURL, replace `{sessionEncounterId}` with your session Encounter id. **TypeScript** {ExampleCode} **CLI** {ExampleCode} **cURL** {ExampleCode} ### See Also - [Encounter](/docs/api/fhir/resources/encounter) FHIR resource API - [Communication](/docs/api/fhir/resources/communication) FHIR resource API - [Messaging Data Model](/docs/communications/messaging-data-model) — thread headers and messages - [Searching and Querying Threads](/docs/communications/searching-and-querying-threads) — finding thread headers and messages - [Family Relationships](/docs/fhir-datastore/family-relationships) — participants and related persons --- ## Creating Your First Thread import ExampleCode from '!!raw-loader!@site/..//examples/src/communications/messaging-examples.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ## Creating Your First Thread: A Step-By-Step Guide This walkthrough creates a thread through the FHIR API using [`MedplumClient`](/docs/sdk/core.medplumclient). That matches how a real app (or Bot) writes messages. The [Medplum App](https://app.medplum.com) is useful as an inspector: confirm resources, references, and history—not as the primary authoring experience you ship to users. Before you start, read how thread headers and messages differ in [Messaging Data Model — Building and Structuring Threads](/docs/communications/messaging-data-model#building-and-structuring-threads). The example below is a provider–patient conversation: the patient is `subject` and is included in `recipient` on the thread header and on messages where they should see or receive the thread, alongside the clinician. ### Prerequisites - An authenticated `MedplumClient`. For a small local script or this exercise, [client credentials](#authenticate-with-client-credentials) below is usually the fastest path. For interactive login, bots, and other flows, see [Auth](/docs/auth) and [`MedplumClient`](/docs/sdk/core.medplumclient). If you still need a Medplum project, use [Register](/docs/tutorials/register). - Existing [`Patient`](/docs/api/fhir/resources/patient) and [`Practitioner`](/docs/api/fhir/resources/practitioner) resources in that project whose `id` values match the snippet references (or change every `reference` in the code to match your data). Create or import them in the [Medplum App](https://app.medplum.com) or another flow you use for test data. Any reference that does not resolve causes `createResource` to fail with a server error pointing at the bad reference. If that happens, confirm the targets exist using the checks in [Verification](#verification) below. #### Authenticate With Client Credentials `MedplumClient` can authenticate in several ways. For machine-to-machine access (typical for a one-off script that creates threads), use OAuth2 client credentials: 1. In the [Medplum App](https://app.medplum.com), open Project Admin → [Clients](https://app.medplum.com/admin/clients) and ensure you have a [`ClientApplication`](/docs/api/fhir/medplum/clientapplication). Create one there if needed (same place you would configure or invite a client for API access). You need the application ID and Secret; see [Client Credentials](/docs/auth/client-credentials) for details and for self-hosted `baseUrl` / token endpoint notes. 2. Instantiate the client and call [`startClientLogin`](/docs/sdk/core.medplumclient.startclientlogin) before any `createResource` or search calls. Use `import { MedplumClient } from '@medplum/core';` at the top of your script (same as the thread snippets below). {ExampleCode} Replace the placeholders with the id and secret from the App. In your own repo, prefer reading them from the environment (for example `process.env.MEDPLUM_CLIENT_ID`) so secrets are not committed. The [`ClientApplication`](/docs/api/fhir/medplum/clientapplication) must be allowed to create and read the resources you use (for example [`Communication`](/docs/api/fhir/resources/communication), `Patient`, `Practitioner`) via [access policies](/docs/access/access-policies) on its project membership—otherwise requests will fail with 401/403 before you get to reference errors. ### Create the Thread Header and Messages 1. Thread header — A [`Communication`](/docs/api/fhir/resources/communication) with no `payload` and no `partOf`. It groups the conversation and carries `topic`, `subject`, participants, and optional `category` when you need tags (see [Messaging Data Model](/docs/communications/messaging-data-model#how-to-tag-or-group-threads)). 2. Each message — A `Communication` with `payload` (content) and `partOf` pointing at the same thread header, plus `sender` and `recipient` as needed. This walkthrough omits `topic` and `category` on child messages to keep the example minimal; duplicate those on children when you need message-level display, search, or subscriptions aligned with the header ([Messaging Data Model](/docs/communications/messaging-data-model#building-and-structuring-threads)). This walkthrough sets `subject` only on the thread header; add `subject` on child messages too if your searches or profiles require it. The first snippet creates the thread header and the opening message from the clinician to the patient. The header lists both participants in `recipient`; see the modeling note in [Messaging Data Model](/docs/communications/messaging-data-model#building-and-structuring-threads). {ExampleCode} #### Reply From Another Participant The next message is from the patient. In a real deployment you would normally create it from another app or login (for example the patient’s session) with its own authenticated `MedplumClient`. The block below keeps it in one tutorial file so you can run the flow locally; it reuses `createdThreadHeader` from the previous step. {ExampleCode} #### Optional: Reply to a Specific Message For linear chat, `partOf` plus sorting by `sent` is usually enough. Add [`inResponseTo`](/docs/api/fhir/resources/communication) only when the user explicitly replies to one message (for example a Reply action). The following snippet continues the variables from the steps above (`createdThreadHeader`, `walkthroughSecondMessage`): {ExampleCode} ### Atomic Creates With Transaction Bundles The steps above use separate `createResource` calls so each part of the thread is obvious. In production, you may prefer a single FHIR `transaction` `Bundle` so the thread header and first message are created together (all entries succeed or none are committed). Use `fullUrl` and `urn:uuid` references so the first message’s `partOf` can point at the header inside the same bundle. This tutorial does not include a full bundle example on purpose: step-by-step `transaction` patterns, the `executeBatch` helper on [`MedplumClient`](/docs/sdk/core.medplumclient), project feature flags, and current limitations are covered in [FHIR Batch Requests](/docs/fhir-datastore/fhir-batch-requests). ### Verification In the [Medplum App](https://app.medplum.com), open Communication and find the new resources. The thread header should have no `partOf` and no `payload`; each message should have both. If `createResource` failed with a reference error, confirm the `Patient` and `Practitioner` from the snippet exist (for example open `Patient/homer-simpson` in the App), or read them with the client: {ExampleCode} To list messages in the thread (newest last when using `_sort=sent`), run a search with the header’s id: **Typescript** {ExampleCode} **CLI** {ExampleCode} **cURL** {ExampleCode} Replace `threadHeader.id` (TypeScript) or `{threadHeaderId}` (CLI/cURL) with your thread header’s id. If nothing appears, check [access policies](/docs/access/access-policies)—your user must be allowed to read those `Communication` resources. ### See Also - [Communication](/docs/api/fhir/resources/communication) FHIR resource API - [Client Credentials](/docs/auth/client-credentials) — OAuth2 client credentials and `ClientApplication` setup - [FHIR Batch Requests](/docs/fhir-datastore/fhir-batch-requests) — batch and transaction `Bundle` requests, `executeBatch` - [Searching and Querying Threads](/docs/communications/searching-and-querying-threads) — thread lists, filters, `_revinclude`, subscriptions - [Sending Messages and Attachments](/docs/communications/sending-messages-and-attachments) — `payload`, files, `Binary` --- ## External Messaging Integration Patterns import ExampleCode from '!!raw-loader!@site/..//examples/src/communications/messaging-examples.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; These patterns show how to bridge Medplum messaging to external channels (SMS, email, push notifications) using [Bots](/docs/bots/bot-basics) and on-demand [`$execute`](/docs/api/fhir/operations/bot-execute). For in-platform automations (Tasks, reminders, scheduling), see [Messaging Automations](/docs/communications/messaging-automations). :::note[Prerequisites] Bots are an [advanced feature](/docs/bots/bot-basics) that may need to be enabled for your project. ::: ### Outbound: Notify External Systems on New Messages ```mermaid flowchart LR CreateComm["App creates child
Communication"] --> Exec["Bot $execute
(notification handler)"] Exec --> External["SMS to patient
via Twilio (example)"] ``` Create a Bot that sends an external notification for a child `Communication` (same handler whether you trigger it from your app server or from another Bot). The handler reads `event.input` as the `Communication` resource. {ExampleCode} After you create the in-app message, invoke the Bot with [`executeBot()`](/docs/sdk/core.medplumclient) so provider API errors (invalid number, throttling, etc.) return to the caller instead of failing asynchronously behind a subscription. {ExampleCode} The example uses [`createResourceIfNoneExist()`](/docs/sdk/core.medplumclient) with a stable application message id so a retry of the same send does not insert duplicate `Communication` rows. See [Bot $execute](/docs/api/fhir/operations/bot-execute) for authentication options, content types, and async execution. :::tip[Processing Notification Bundles] When you handle live updates (WebSocket `Subscription` notifications or similar), locate the new `Communication` by scanning `bundle.entry` for `resourceType === 'Communication'` instead of assuming a fixed entry index. See [Searching and Querying Threads](/docs/communications/searching-and-querying-threads#live-updates). ::: ### Inbound: Create Messages from External Webhooks ```mermaid flowchart LR External["Patient texts
'Thanks, I will review them'
via Twilio"] --> Bot["Medplum Bot
(receives webhook)"] Bot --> CreateComm["Create Communication
(partOf → lab results thread)"] ``` Wire inbound provider webhooks to a Bot using [`$execute`](/docs/api/fhir/operations/bot-execute) (see [Consuming Webhooks](/docs/bots/consuming-webhooks)). Some partners send OAuth-signed or otherwise authenticated webhooks; others require a public URL and an [unauthenticated webhook](/docs/bots/consuming-webhooks#unauthenticated-webhooks) endpoint—use the pattern that matches your vendor and lock down access (signatures, narrow AccessPolicy, and validation inside the Bot). When an inbound message arrives from an external channel, the Bot typically solves two problems: sender resolution (who sent this?) and thread matching (which conversation does it belong to?). FHIR [conditional references](https://www.hl7.org/fhir/references.html#conditional) can resolve the sender at write time when the external system provides enough context. #### Sender Resolution with Conditional References Use a conditional reference on `sender` so the server resolves the patient at create time — no separate search step: {ExampleCode} The snippet above only demonstrates sender resolution. It omits `partOf` on purpose — you still match or create a thread using one of the strategies below before you treat the example as a complete inbound message. The conditional reference `Patient?phone=` resolves to exactly one matching [`Patient`](/docs/api/fhir/resources/patient) at write time (no separate search in your Bot). For email, use `Patient?email=
`. :::caution[Conditional References and the Whole `POST`] If any conditional reference on the `Communication` you are creating does not resolve — zero matches or more than one match — the server rejects the entire create. There is no partial write. That applies to `sender` (for example `Patient?phone=...`) and, in Strategy 1 below, to `partOf` (for example `Communication?identifier=|` matching how you indexed the thread header). Order this to match your product rules: - Reject unrecognized inbound senders — Using `Patient?phone=...` (or `Patient?email=...`) without pre-creating the patient is appropriate: the request fails until that phone or email uniquely identifies an existing patient. - Accept messages from new numbers or addresses — Resolve identity first (search, create, or upsert a `Patient`), then create the `Communication` with a normal literal reference such as `Patient/{id}` on `sender`, or with a conditional you know will match. The [`MedplumClient`](/docs/sdk/core.medplumclient) helper `createResourceIfNoneExist()` is described in [Working with FHIR](/docs/fhir-datastore/working-with-fhir) (see Creating Patient — it performs a search-then-conditional-create style flow). For update-or-create in one step, see [Upsert](/docs/fhir-datastore/working-with-fhir#upsert) on the same page. Other fallbacks (such as a `display`-only reference without a resolved `reference`) depend on your validation rules — they may still fail if your server requires a resolved `sender`. ::: Use `Communication.medium` to record which channel the message used. That supports channel indicators in your UI and downstream routing back through the same channel. #### Thread Matching Strategies Sender resolution and thread matching are separate. The conditional reference above answers “who sent this” but you still need to decide which thread the message belongs to. Pick a strategy based on what the external system provides. ##### Strategy 1: External Conversation ID (Recommended When Available) If the external system has its own conversation identifier (Twilio Conversations, email `Message-ID` / `In-Reply-To`, etc.), store that id on the thread header as an `identifier` when the thread is first created. The system and value must match what you pass in the conditional `partOf` reference on inbound messages. Set `medium` on the header if you use round-trip routing (see below). Upsert the thread header (no `partOf`, no `payload` on the header) so setup is idempotent if the same conversation id is seen more than once. [`upsertResource()`](/docs/sdk/core.medplumclient.upsertresource) matches on the same `identifier` search you will use for conditional `partOf`: {ExampleCode} Inbound messages can then use a conditional reference for `partOf`: {ExampleCode} Both `sender` and `partOf` can resolve via conditional references in a single `POST`. The `partOf` reference uses the same `identifier` search shape you stored on the thread header (for example `Communication?identifier=https://twilio.com|` — the system and value must match how you set [`Communication.identifier`](/docs/api/fhir/resources/communication)). If no header matches, that conditional fails and the whole `Communication` create fails, same as an unmatched `Patient?phone`. You must also include the external id on every inbound payload so the server can resolve `partOf`. ##### Strategy 2: Patient + Active Thread Lookup (Fallback for Stateless Channels) If the channel is stateless (basic SMS, email without thread headers), there may be no external conversation id. The Bot resolves the patient, then searches for their most recent open thread header. This example uses the same active-thread filter as other messaging docs (`status:not=completed,entered-in-error,stopped,unknown` — see [Searching and Querying Threads](/docs/communications/searching-and-querying-threads#search-query-cheat-sheet)). The webhook payload must include the provider’s stable message id (`providerMessageId` in the example) for deduplication. {ExampleCode} :::tip[Unknown Inbound Numbers] The handler above returns quietly when no `Patient` matches the phone search. In production, log the raw number, write a dead-letter or audit record, or page an operator so retries and fraud attempts are visible. ::: :::tip[Modeling Thread Headers for Real Apps] When you create a new thread from inbound SMS, consider setting `sender`, `recipient`, and `topic` consistently with [Messaging Data Model](/docs/communications/messaging-data-model) so inbox searches and access policies behave as expected. ::: ##### Strategy 3: New Thread Per Inbound Message Always create a new thread header for each inbound message. No matching logic. This fits one-off acknowledgments where messages are independent rather than a single ongoing conversation. | Strategy | Best for | Tradeoff | | -------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------- | | External conversation ID (conditional) | Platforms with conversation state (Twilio, email threads) | Cleanest and declarative; requires storing the external id on the header | | Patient + active thread lookup | Stateless SMS or simple email | Search logic in the Bot; ambiguous if several open threads exist for one patient | | New thread per message | One-off notifications | Ongoing conversations become fragmented | :::caution[Webhook Retries and Duplicate Messages] Providers such as Twilio and SendGrid often retry webhook delivery. The same inbound event can hit your Bot more than once. Store the provider’s message id (or similar) on the [`Communication`](/docs/api/fhir/resources/communication) as an [`identifier`](/docs/api/fhir/resources/communication) and create with [`createResourceIfNoneExist()`](/docs/sdk/core.medplumclient) or another conditional-create pattern so a retry does not insert a second identical message. ::: ### Round-Trip: Routing Replies Through the Original Channel When the patient reached you on SMS, provider replies composed in your app should often go back out on SMS. After you upsert the thread header with `medium` indicating SMS (as in Strategy 1), persist the provider’s in-app reply as a child `Communication`, then [`$execute`](/docs/api/fhir/operations/bot-execute) the same style of outbound Bot with that resource as input. The Bot inspects the thread header: if `medium` includes SMS, resolve the patient from `subject`, read `Patient.telecom` for a phone number, and call your SMS API. ```mermaid flowchart LR PatientSms["Patient sends SMS"] --> InboundBot["Inbound Bot creates Communication"] InboundBot --> Thread["Thread header with SMS medium"] ProviderReply["Provider creates in-app reply"] --> Exec["Bot $execute
(round-trip handler)"] Exec --> SmsOut["SMS to patient.telecom"] ``` {ExampleCode} Orchestration from your app or API layer: {ExampleCode} Your product may key off the last inbound message’s `medium` instead of the header if that fits your model better. ### See Also - [Communication](/docs/api/fhir/resources/communication) FHIR resource API - [Messaging Data Model](/docs/communications/messaging-data-model) - [Searching and Querying Threads](/docs/communications/searching-and-querying-threads) - [Messaging Automations](/docs/communications/messaging-automations) - [Working with FHIR](/docs/fhir-datastore/working-with-fhir) — conditional create, `createResourceIfNoneExist()`, upsert - [Bots](/docs/bots/bot-basics) and [Bot $execute](/docs/api/fhir/operations/bot-execute) --- ## Messaging & Communications :::tip[Planning this workflow?] The [Messaging & Communications Decision Guide](/docs/decision-guides/messaging) walks through requirements questions and FHIR modeling decisions for messaging — use it alongside these docs. ::: FHIR supports messaging workflows through the [Communication](/docs/api/fhir/resources/communication) resource. This resource allows your system to manage message content and metadata in a format independent of its medium (email, SMS, chat, etc.). For more information on the data model, see [Messaging Data Model](/docs/communications/messaging-data-model). These [Communications](/docs/api/fhir/resources/communication) can also be structured to support a variety of use cases, including: - [Thread lifecycle, participants, and access control](/docs/communications/thread-lifecycle-participants-access-control) - [Searching and querying message threads](/docs/communications/searching-and-querying-threads) - [Message Response Tracking and Routing](/docs/communications/message-response-tracking-and-routing) - [Message attachments](/docs/communications/sending-messages-and-attachments) - [Message editing and drafts](/docs/communications/message-editing-and-drafts) - [Read receipts and message status](/docs/communications/read-receipts-and-message-status) - [Messaging automations](/docs/communications/messaging-automations) - [Omni-channel messaging](/docs/communications/external-messaging-integration-patterns) (SMS, email, webhooks) - [Asynchronous encounters](/docs/communications/async-encounters) - Real-time notifications using Websockets This page covers: - The `Consent` resource and how to code one - Auditing who recorded or changed a consent - Capturing consents through a questionnaire - Capturing a handwritten digital signature and linking it to a consent ### The Consent Resource A single `Consent` represents one agreement. Do not bundle several agreements into one resource — a patient who accepts treatment but declines a payment agreement needs two independently statused records. | Element | Purpose | | --- | --- | | `status` | `draft`, `proposed`, `active`, `rejected`, `inactive`, or `entered-in-error`. A declined agreement is a `rejected` Consent, not a missing one. | | `scope` | The broad context of the consent — privacy, treatment, research, or advance care directive. Required. | | `category` | The type of consent document. Required, and repeatable so you can carry both a LOINC document type and an act code. | | `patient` | The patient the consent is about. | | `dateTime` | When the consent was given. This is the clinically meaningful date, distinct from `meta.lastUpdated`. | | `performer` | Who granted the consent — the patient, or a `RelatedPerson` acting for them. | | `organization` | The organization holding the consent. | | `policy` | The governing agreement, as an `authority` plus a `uri` pointing at the actual document. | | `policyRule` | A coded reference to a known regulatory policy. | | `sourceAttachment` / `sourceReference` | The underlying artifact — see [Linking a Signature to a Consent](#linking-a-signature-to-a-consent). | | `verification` | Whether the consent was verified, with whom, and on what date. | | `provision` | Fine-grained permit/deny rules. Most consent capture flows do not populate this. | The example below is a privacy consent that governs data disclosure to a third-party e-prescribing vendor. Note that it uses `policy` with a live `uri` rather than `policyRule`, because the governing document is the vendor's terms of service rather than a coded regulation: ```json { "resourceType": "Consent", "status": "active", "scope": { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/consentscope", "code": "patient-privacy", "display": "Patient Privacy" } ], "text": "Patient Privacy" }, "category": [ { "coding": [ { "system": "http://loinc.org", "code": "59284-0", "display": "Privacy Consent Document" } ] }, { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", "code": "IDSCL", "display": "information disclosure" } ] } ], "patient": { "reference": "Patient/e9232f44-6287-4506-8bbc-b4091a284054", "display": "Frodo Baggins" }, "dateTime": "2026-07-28T21:28:10.866Z", "policy": [ { "authority": "DoseSpot", "uri": "https://dosespot.com/exhibit-a-terms-of-service/" } ] } ``` #### Coding a Consent Consent coding is where most implementations diverge, because it encodes organizational policy rather than clinical fact. Three elements carry the meaning: - **`scope`** uses [consentscope](http://terminology.hl7.org/CodeSystem/consentscope) — `patient-privacy`, `treatment`, `research`, or `adr`. - **`category`** identifies the document type. LOINC `59284-0` (Privacy Consent Document) is common, as are [v3-ActCode](http://terminology.hl7.org/CodeSystem/v3-ActCode) values such as `nopp`, `pay`, and `med`. - **`policyRule`** points at a coded policy such as `hipaa-npp` or `hipaa-self-pay`. Use `policy` with an `authority` and `uri` instead when the governing document is a specific contract or terms-of-service page. For a table mapping common intake consent types to specific scope, category, and policyRule combinations, see [modeling Consent resources in intake questionnaires](/docs/intake/intake-questionnaires#modeling-consent-resources). :::note[Coding varies by organization] The code systems above are HL7 standards, but which combination represents "consent for treatment" at your organization is a policy decision. Agree on the coding before building extraction logic, because changing it later means migrating historical consents. ::: #### Status and Re-consent Consent is not a one-time event. Policies change, patients move between sites, and many organizations re-consent annually. Model this as a sequence of resources rather than by editing one in place: - The patient agrees → `status: active` - The patient declines → `status: rejected` (still a recorded decision, and still auditable) - A newer consent supersedes it → the prior Consent moves to `status: inactive` and a new Consent is created with the current `dateTime` - The consent was recorded in error → `status: entered-in-error` Superseding rather than editing preserves the answer to "what had this patient agreed to on a given date," which is the question an audit actually asks. ### Auditing Consent Changes Medplum stores every version of every resource, so a consent carries its own audit trail with no additional configuration. In the [Medplum App](https://app.medplum.com), open a Consent and use: - **History** — every version, with the timestamp and the user account responsible. Selecting a version shows a diff against the prior one. Available directly at `https://app.medplum.com/Consent/:id/history`. - **Blame** — the current resource annotated line by line with the version and user that last touched each field. This answers "who changed the status of this consent, and when" without reading through diffs. - **JSON** — the raw resource, including the code systems and `meta.lastUpdated`. The same history is available over the API: ```bash GET /fhir/R4/Consent/:id/_history ``` See [Resource History](/docs/fhir-datastore/resource-history) for the full history API, including reading a specific version. ### Capturing Consent in a Form Most consents are captured as part of a larger form rather than on their own. In a patient intake flow, each acknowledgement in the questionnaire becomes its own Consent resource: | Intake acknowledgement | `scope` | `category` | | --- | --- | --- | | Consent for Treatment | `treatment` | `med` | | Agreement to Pay for Treatment | `treatment` | `pay` | | Notice of Privacy Practices | `patient-privacy` | `nopp` | | Acknowledgement for Advance Directives | `adr` | `acd` | Two mappings matter when you write the extraction logic: - The **checkbox** drives `status` — checked becomes `active`, unchecked becomes `rejected`. Creating nothing when the box is unchecked loses the fact that the patient was asked and declined. - The **date field** drives `dateTime`, not `meta.lastUpdated`. A form completed on paper and entered later has a consent date earlier than its creation timestamp. Extraction runs either through SDC `extract` extensions or through a Bot. For the mechanics of both, along with linkId conventions, see [Intake Questionnaires: Design and Extraction](/docs/intake/intake-questionnaires). For how Consent fits the wider set of resources an intake response produces, see the [Intake Data Model](/docs/intake/intake-data-model). The clinician-facing walkthrough of this step is in the [Provider app patient profile guide](/docs/provider/patient-profile). ### Capturing a Digital Signature Some consents need a handwritten signature rather than a checkbox. Add the `questionnaire-signatureRequired` extension to a [`Questionnaire`](/docs/api/fhir/resources/questionnaire) and the `QuestionnaireForm` component renders a signature pad below the form and blocks submission until it is signed: ```json { "resourceType": "Questionnaire", "status": "active", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-signatureRequired", "valueCodeableConcept": { "coding": [ { "system": "urn:iso-astm:E1762-95:2013", "code": "1.2.840.10065.1.12.1.1", "display": "Author's Signature" } ] } } ], "item": [{ "linkId": "consent-text", "type": "display", "text": "..." }] } ``` Pair the pad with a plain `string` item labeled "Print Name" when your compliance posture requires the signer's printed name alongside the mark. #### Where the Signature Is Stored On submit, the signature is written to the [`QuestionnaireResponse`](/docs/api/fhir/resources/questionnaireresponse) as a `questionnaireresponse-signature` extension holding a FHIR `Signature`: ```json { "resourceType": "QuestionnaireResponse", "status": "completed", "authored": "2026-07-28T21:28:10.866Z", "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/questionnaireresponse-signature", "valueSignature": { "type": [ { "system": "http://hl7.org/fhir/signature-type", "code": "ProofOfOrigin", "display": "Proof of Origin" } ], "when": "2026-07-28T21:28:10.866Z", "who": { "reference": "Practitioner/dc1c2b39-9dd5-4f4a-8e7d-2b1e3a5c6d70" }, "data": "iVBORw0KGgoAAAANSUhEUg..." } } ] } ``` The three fields that make this an evidentiary record are `who` (the authenticated profile that signed, defaulting to the logged-in user), `when` (the moment the stroke completed), and `data` (the signature image as a base64-encoded PNG). The submitted response also carries `source` and `authored`, so the signing identity is recorded independently of the signature itself. :::caution[Signature capture and paginated forms] The signature pad renders only on single-page questionnaires. If a questionnaire uses pagination *and* carries the `signatureRequired` extension, submission is blocked but no pad is shown — leaving the form unsubmittable. Keep signature-required questionnaires unpaginated, or collect the signature in a separate step. ::: To use the pad outside a questionnaire — in a custom consent screen, for example — render the [`SignatureInput`](https://storybook.medplum.com/?path=/story/medplum-signatureinput--basic) component directly. It accepts a `who` reference so you can attribute the signature to a patient rather than the logged-in user. Working examples are in Storybook: [QuestionnaireForm with a required signature](https://storybook.medplum.com/?path=/story/medplum-questionnaireform--signature-required) and [SignatureInput on its own](https://storybook.medplum.com/?path=/story/medplum-signatureinput--basic). ### Linking a Signature to a Consent A signature captured on a QuestionnaireResponse is not automatically attached to the Consent resources extracted from it. Creating that link is part of your extraction logic, and it is what makes the consent record self-contained. Two options: **Reference the response.** `Consent.sourceReference` accepts a `QuestionnaireResponse`, so the consent can point at the exact submission that produced it — signature extension included: ```json { "resourceType": "Consent", "status": "active", "patient": { "reference": "Patient/e9232f44-6287-4506-8bbc-b4091a284054" }, "dateTime": "2026-07-28T21:28:10.866Z", "sourceReference": { "reference": "QuestionnaireResponse/a1b2c3d4-5678-90ab-cdef-1234567890ab" } } ``` This is the lighter-weight option and keeps one signature serving every consent extracted from the form. **Attach the artifact.** Where each consent needs to stand alone — for disclosure to a payer, or export to another system — store the rendered document as a [`Binary`](/docs/api/fhir/resources/binary) and reference it from `Consent.sourceAttachment`. See [Binary Data](/docs/fhir-datastore/binary-data) for upload mechanics. Reference the response when the form is the record of truth; attach the artifact when the consent must travel on its own. ### Compliance Considerations Electronic signatures under [21 CFR Part 11](/docs/compliance/cfr11) §11.50 must record the signer's printed name, the date and time, and the meaning of the signature. The captured `Signature` maps onto these directly: | Requirement | Where it lives | | --- | --- | | Identity of the signer | `Signature.who`, plus `QuestionnaireResponse.source` | | Date and time of signing | `Signature.when` | | Meaning of the signature | `Signature.type`, and the `signatureRequired` extension's declared type | | Printed name | A `string` item in the questionnaire, resolved through `Signature.who` | | Signature/record linking (§11.70) | `Consent.sourceReference` or `sourceAttachment`, plus resource versioning | Resource history supplies the tamper-evidence: because Medplum retains every version with its author, an altered consent shows the change, the author, and the timestamp. Assess whether this satisfies your specific regulatory obligations with your compliance team — see the [compliance overview](/docs/compliance) and [HIPAA](/docs/compliance/hipaa) for Medplum's broader posture. ### Related Materials - [Consent Capture in Medplum](https://youtu.be/9Ck2FEa4ALg) on YouTube — walkthrough of the Consent resource, its audit trail, intake capture, and signature panels - [Intake Questionnaires: Design and Extraction](/docs/intake/intake-questionnaires) — modeling consents captured during intake - [Intake Data Model](/docs/intake/intake-data-model) — the full resource graph an intake response produces - [Questionnaires and Responses](/docs/questionnaires/questionnaires-and-responses) — the `signatureRequired` extension in the wider questionnaire feature set - [Intake and Registration decision guide](/docs/decision-guides/intake) — design questions for conditional consents and re-consent - [Resource History](/docs/fhir-datastore/resource-history) — the versioning that underpins the consent audit trail - [21 CFR Part 11](/docs/compliance/cfr11) — electronic records and signatures compliance matrix - [`Consent` resource reference](/docs/api/fhir/resources/consent) — full element and search parameter listing --- ## Running AWS Lambda Bots with Localhost When developing for Medplum, you may want your **local Medplum Server** to trigger and execute **AWS Lambda-based Bots**. This setup allows you to test Lambda-specific behavior (like cold starts or resource limits) without deploying your entire server stack to the cloud. #### Architecture Overview When configured this way, your local server acts as the "orchestrator," using your local AWS credentials to invoke Lambdas in your AWS environment. ### Setup Instructions #### 1. Configure AWS Credentials Your local server needs permission to create and invoke Lambda functions. Medplum recommends using **AWS IAM Identity Center (SSO)** for secure access. To authenticate with a specific profile: ```bash aws sso login --profile medplum-dev ``` To verify your identity and ensure your session is active: ```bash aws sts get-caller-identity --profile medplum-dev ``` > **Note:** If you aren't using SSO, ensure your `~/.aws/credentials` file is properly configured with an `access_key` and `secret_key`. #### 2. Configure the Medplum Server Update your `packages/server/medplum.config.json` to point to the correct AWS resources. ```json { "botLambdaRoleArn": "arn:aws:iam::123456789012:role/MedplumBotRole", "botLambdaLayerName": "medplum-bot-layer" } ``` ##### How to find these values: The most efficient way to get these values is to pull them from an existing environment's **AWS Parameter Store**: 1. Open the **AWS Console** and navigate to **Systems Manager > Parameter Store**. 2. Look for the following keys (standard for Medplum deployments): - `/medplum/staging/botLambdaRoleArn` - `/medplum/staging/botLambdaLayerName` #### 3. Start the Local Server When starting your server, you must explicitly pass the `AWS_PROFILE` so the AWS SDK can find your credentials. ```bash # From the root of the medplum repository AWS_PROFILE=medplum-dev npm run dev --workspace=packages/server ``` #### 4. Deploy and Execute Once the server is running with the correct configuration: 1. **Create/Open a Bot:** In your local Medplum web app (typically `localhost:3000`), navigate to your Bot. 2. **Set Runtime:** Ensure the `Bot.runtimeVersion` is set to `awslambda`. 3. **Deploy:** Click **"Deploy"**. The local server will now bundle your bot and upload it as a Lambda function to your AWS account. 4. **Execute:** Click **"Execute"**. You should see the execution logs in your local terminal and the Medplum UI. ### Troubleshooting - **Permissions:** Ensure the IAM Role defined in `botLambdaRoleArn` has the `lambda:InvokeFunction` and `lambda:CreateFunction` permissions. - **Region:** Ensure your `AWS_REGION` is correctly set in your environment variables or AWS config file. ### See Also - [Medplum Bot Basics](/docs/bots/bot-basics) - [Provisioning Bot Layers](/docs/bots/bot-lambda-layer) - [AWS CLI Configuration Guide](https://docs.aws.amazon.com/cli/v1/userguide/cli-chap-configure.html) --- ## Code of Conduct ### Purpose and Scope This Code of Conduct outlines Medplum's expectations measured against the highest possible standards of ethical business conduct. Committing to the highest standards helps Medplum hire great people, build great products, and attract loyal customers. From time to time, Medplum may update this Code of Conduct. This policy is guided by requirements specific to Medplum including applicable laws and regulations. We expect all Medplum personnel to review, understand, and abide by this Code of Conduct in all matters related to Medplum. ### Respect in the Workplace All personnel must respect their colleagues. Medplum will not allow any kind of discriminatory behavior, harassment, or victimization. Medplum prohibits retaliation against any personnel who report or participate in an investigation of a possible violation of Medplum's Code of Conduct, policies, or the law. If you believe you are being retaliated against, please contact Reshma Ebberson, another senior manager, or anonymously at [security@medplum.com](mailto:security@medplum.com). Personnel must use company assets as outline in the Acceptable Use Policy. This includes safe handling of trademarks, copyright, and other property (information, reports, etc.). ### Safe Workplace Medplum is committed to a violence-free work environment, and we will not tolerate any level of violence or the threat of violence in the workplace. Under no circumstances should anyone bring any type of weapon to work including guns, explosives, or knives. If you become aware of a violation of this policy, you should report it to a member of management immediately. In the case of potential physical violence, contact the authorities immediately. ### Workplace Visitors Workplace safety is very important to Medplum. As Medplum visitors access workplace premises, Medplum should ensure that visitors are not a threat to the workplace and are not exposed to danger. ### Equal Opportunity Employment Medplum is an equal opportunity employer. Medplum thrives on diversity and are committed to creating an inclusive environment for all personnel. ### Professionalism All personnel must show integrity and professionalism in the workplace. #### Job Duties and Authority All personnel should fulfill their job duties with integrity and respect toward customers, stakeholders and the community. Supervisors and managers must not abuse their authority. We encourage mentorship throughout Medplum. #### Communication and Collaboration Personnel should be responsive and open for communication with their colleagues, supervisors, and team members. Personnel should be friendly and collaborative and not disrupt the workplace or hinder their colleagues' work. #### Benefits Medplum expects employees to not abuse their employment benefits. Please reach out to Human Resources for questions pertaining to company benefits. #### Compliance with Law Personnel must comply with all applicable laws including environmental, safety and fair dealing laws. Medplum expects everyone to be ethical and responsible during Medplum business dealings. ### Conflict of Interest Conflicts of interest occur when an employee, contractor, or job applicant's personal interests may not align with company needs or interests. We expect you to avoid any personal, financial, or other interests that might hinder your capability or willingness to perform your job duties. If you believe that a conflict may occur, please contact your manager immediately. Types of conflicts of interest may include: - Personal investments - Outside employment, advisory roles, board seats, and starting your own business - Business opportunities found through work - Inventions - Accepting gifts, entertainment, and other business courtesies ### Internet and Social Media Personnel should never share any intellectual property or the status of any of their assignments on social media. When representing the company, personnel should always be respectful and avoid speaking in specifics about their work. Personnel should never post discriminatory, offensive, or other illegal language on social media. ### Exceptions Medplum business needs, local situations, laws, and regulations may occasionally call for an exception to this policy or any other Medplum policy. If an exception is needed, Medplum management will determine an acceptable alternative approach. ### Enforcement Any violation of this policy or any other Medplum policy or procedure may result in disciplinary action, up to and including termination of employment. Medplum reserves the right to notify the appropriate law enforcement authorities of any unlawful activity and to cooperate in any investigation of such activity. Medplum does not consider conduct in violation of this policy to be within an employee's or contractor's course and scope of work. Any employee or contractor who is requested to undertake an activity that he or she believes is in violation of this policy must provide a written or verbal complaint to his or her manager or any other manager of Medplum as soon as possible. The disciplinary process should also be used as a deterrent to prevent employees and contractors from violating organizational security policies and procedures, and any other security breaches. ### Responsibility, Review, and Audit Medplum reviews and updates its security policies and plans to maintain organizational security objectives and meet regulatory requirements at least annually. The results are shared with appropriate parties internally and findings are tracked to resolution. Any changes are communicated across the organization. This document is maintained by Reshma Ebberson. This document was last updated on 02/28/2025. --- ## Developer Certificate of Origin At Medplum, we've enabled the Developer Certificate of Origin (DCO) for all contributions to our repositories. This means every commit to the Medplum codebase needs to be "signed off" by the author. We understand this might be a new process for some, and we're here to help you get started. ### What is DCO and Why Do We Use It? The **Developer Certificate of Origin (DCO)** is a lightweight attestation from a contributor that certifies they have the right to submit the code they are contributing to the project. It's an industry-standard practice in many open-source projects, including the Linux kernel, to ensure a clear chain of ownership and licensing. Instead of a formal Contributor License Agreement (CLA) that requires a separate legal agreement, the DCO works by adding a simple line to your commit message: ``` Signed-off-by: Your Name ``` By including this line, you are certifying that: 1. **You created the contribution** in whole or in part, and you have the right to submit it under the project's open-source license. 2. **The contribution is based on previous work** that, to the best of your knowledge, is covered under an appropriate open-source license, and you have the right to submit that work with modifications. 3. **You understand and agree** that this project and your contribution are public and that a record of your contribution (including your sign-off) is maintained indefinitely and may be redistributed. #### Why Medplum Requires DCO As an open-source healthcare developer platform, maintaining a clear and verifiable history of contributions is crucial for Medplum. DCO helps us: - **Protect our users and ourselves:** It minimizes legal risks related to intellectual property and licensing, ensuring that all code contributed to Medplum can be freely used and distributed under our chosen open-source license. - **Maintain transparency:** It provides a transparent record of who contributed what, which is essential for community trust and accountability. - **Streamline contributions:** It's a less burdensome alternative to a full CLA, making it easier for new contributors to get involved without extensive legal overhead. --- ### How to Sign Off Your Commits The DCO sign-off needs to be present on _every commit_ in a pull request. Here are the simplest ways to ensure your commits are signed off: #### 1\. Signing Off New Commits (Recommended) The easiest way to sign off your commits is to use the `-s` or `--signoff` flag with your `git commit` command. This automatically appends the `Signed-off-by:` line using your Git configured user name and email. First, make sure your Git user name and email are correctly configured: ```bash git config --global user.name "Your Name" git config --global user.email "your.email@example.com" ``` Then, for new commits: ```bash git commit -s -m "Your commit message here" ``` If you prefer the verbose option: ```bash git commit --signoff -m "Your commit message here" ``` #### 2\. Fixing Existing Commits (If You Forgot) It's common to forget the `-s` flag sometimes, especially when you're just getting used to it. Don't worry, you can fix it\! ##### Option A: Amending the Last Commit If you only forgot the sign-off on your very last commit and haven't pushed it yet: ```bash git commit --amend --signoff ``` This command will open your commit message in an editor, where you'll see the `Signed-off-by:` line automatically added. Save and close the editor. If you've already pushed this commit, you'll need to force push: ```bash git push -f origin your-branch-name ``` **Use `git push -f` with caution\!** Force pushing overwrites history on the remote branch. Only do this if you are sure no one else has based work off your branch. ##### Option B: Rebasing to Sign Off Multiple Commits If you have multiple commits in your branch that are missing the sign-off, you'll need to perform an interactive rebase. This allows you to rewrite the history of your commits. Let's say you want to sign off the last 3 commits. Replace `HEAD~3` with the appropriate number of commits you need to rebase: ```bash git rebase -i HEAD~3 ``` This will open an editor showing your last N commits. Change `pick` to `reword` (or `r`) next to each commit you want to modify. ``` pick a1b2c3d Commit 1 message pick e4f5g6h Commit 2 message pick i7j8k9l Commit 3 message ``` Change to: ``` reword a1b2c3d Commit 1 message reword e4f5g6h Commit 2 message reword i7j8k9l Commit 3 message ``` Save and close the editor. Git will then go through each commit one by one, opening your editor for you to modify the commit message. For each one, add the `-s` flag to the `git commit` command that appears: ```bash git commit -s --amend ``` After modifying all the commits, you'll need to force push your branch: ```bash git push -f origin your-branch-name ``` --- ### Tips and Tricks for Convenience While there's no single `git config` option to _always_ sign off every commit automatically, here are some helpful tricks: #### 1\. Git Alias for Signed Commits This is a popular solution. You can create a Git alias that includes the `-s` flag, giving you a shorter, more memorable command for signed commits. To set up a global alias for `scommit`: ```bash git config --global alias.scommit 'commit -s' ``` Now, instead of `git commit -s -m "..."`, you can just type: ```bash git scommit -m "Your commit message here" ``` #### 2\. Git Hooks (Advanced) For more advanced users, you can use a Git hook, specifically the `prepare-commit-msg` hook, to automatically add the `Signed-off-by:` line if it's missing. This would be a per-repository setup. To do this, create or edit the file `.git/hooks/prepare-commit-msg` in your repository and add the following script: ```bash #!/bin/sh NAME=$(git config user.name) EMAIL=$(git config user.email) if [ -z "$NAME" ] || [ -z "$EMAIL" ]; then echo "Error: Git user.name and user.email must be configured for DCO sign-off." exit 1 fi if ! grep -qs "^Signed-off-by:" "$1"; then printf "\nSigned-off-by: %s <%s>\n" "$NAME" "$EMAIL" >> "$1" fi ``` Make sure the hook is executable: ```bash chmod +x .git/hooks/prepare-commit-msg ``` This script will check if a `Signed-off-by:` line exists and add it if not. It will also ensure your `user.name` and `user.email` are set. --- ## Contributing to Medplum Medplum is an open-source project, and we love both code and non-code contributions! People with any level of experience can make an important impact on the project: you don't need to be a professional developer! You might have just: - **Ten minutes** to write a GitHub issue describing a bug you found or request for a feature you could use - **One hour** to learn and participate in one of our webinars - **Four hours** to author a case study showcasing how you've used Medplum This section provides instructions on how to get started contributing to the Medplum project. If you're looking to _use_ Medplum, check out the [App](./app) or [API](./api) docs instead. ### Join our online community We have several online venues where Medplum community members and team members convene, offering various ways to get involved with the project: 1. Star the [Medplum Github repository](https://github.com/medplum/medplum) 2. Join our [Discord channel](https://discord.gg/medplum) - and introduce yourself 3. Subscribe to [Medplum on LinkedIn](https://www.linkedin.com/company/medplum) 4. Follow [Medplum on Twitter](https://twitter.com/Medplum1) 5. Subscribe to our [Youtube channel](https://www.youtube.com/channel/UCu_sS6aXEHz3GPk2NTugtJA) #### Spread the word and participate Help us reach and engage people passionate about building amazing healthcare solutions! 1. Let us know your thoughts on [Github Discussions](https://github.com/medplum/medplum/discussions) 2. Share our [blog posts](/blog) 3. Share code, posts, videos or other content in relevant online forums for example [dev.to](https://dev.to/), [/r/healthIT](https://www.reddit.com/r/healthIT/), [/r/selfhosted](https://www.reddit.com/r/selfhosted/), or [Hacker News](https://news.ycombinator.com) 4. Write content for your own blog or website, and [tell us about it](mailto:hello@medplum.com) ### Making a contribution There are several ways to make your first contribution to the Medplum project! The following instructions should help you get started. #### Reporting a bug or discussing a feature idea If you found a technical bug on Medplum or have ideas for features we should implement, the [GitHub issue tracker](https://github.com/medplum/medplum/issues) is the best place to share your ideas. [Open a new issue](https://github.com/medplum/medplum/issues/new) describing the changes you'd like to see, and one of our team members will respond. #### Writing documentation or blog content Did you learn how to do something using Medplum but it wasn't obvious on first try? Please contribute to our documentation! Our documentation is hosted on [medplum.com/docs](/docs), but it is built from [Markdown](https://www.markdownguide.org/) files that live in our [Github repository](https://github.com/medplum/medplum/tree/main/packages/docs/docs). For relatively small changes, you can edit files directly from your web browser on [github.dev](https://github.dev/medplum/medplum/blob/main/packages/docs/docs/home) without needing to clone the repository. #### Fixing a bug or implementing a new feature If you find a bug on Medplum and open a GitHub Pull Request that fixes it, we'll review it as soon as possible. Before starting implementation of a new feature, open an issue first to discuss your plans and to ensure it fits into our roadmap and plans for the app. If you're not yet an established contributor, your pull request needs to be linked to an issue labeled `open-to-community` (for example with a "Fixes #123" or "Closes #123" reference, or via the GitHub "Development" panel) -- this shows we've explicitly invited a community PR for that work. A maintainer can add this label to an existing issue, including one you just opened, on request. Pull requests that aren't linked to a qualifying issue are closed automatically; just reopen it once it's linked, or open the issue first if one doesn't exist yet. A maintainer can also approve a pull request directly by adding the `vouched` label to it; labeled pull requests aren't closed automatically. Medplum team members and established community contributors aren't subject to this requirement. **Ready to get started writing code?** First things first, you need to [clone the Medplum repository](./contributing/local-dev-setup). :::note[Legal Note] By submitting content to this project, you agree to adopt the [Developer Certificate of Origin (DCO)](https://developercertificate.org/) for your contributions. Medplum personnel and contractors are expected to follow the Medplum [Code of Conduct](./contributing/code-of-conduct). All conversations and communities on Medplum are expected to follow GitHub's [Community Guidelines](https://help.github.com/en/github/site-policy/github-community-guidelines) and [Acceptable Use Policies](https://help.github.com/en/github/site-policy/github-acceptable-use-policies). ::: --- ## Local development setup Ready to get started developing Medplum on your local machine? Great! First, you'll need to set up your own copy of the [Medplum repository](http://github.com/medplum/medplum) to work on, by [forking the repository](https://docs.github.com/en/get-started/quickstart/fork-a-repo) into your own GitHub account. **Note:** If you don't want to make changes and submit them back to the Medplum project, you can skip this step and proceed directly to the next step. After you've created your own fork of the Medplum repository, copy it to your local machine to work on by cloning it. ### Clone the repo If you created a fork to submit Pull Requests from, you'll need to ensure you clone your fork, rather than the official Medplum repository. Otherwise, use `git@github.com:medplum/medplum.git` below to copy the official repository, which you can use to build and run the app locally. ```bash git clone git@github.com:{YOUR GITHUB USERNAME}/medplum.git medplum ``` If you get an error saying `Permission denied` using `ssh`, you can refer to the [Github docs on setting up SSH keys](https://help.github.com/articles/error-permission-denied-publickey/). Alternatively, you can use the `https` method as a fallback. ```sh git clone https://github.com/{YOUR GITHUB USERNAME}/medplum.git medplum ``` That will create a complete copy of the project source code on your local machine, including code for the Medplum Server, Medplum App, and associated libraries. In the next step, we'll build the application and run the tests. ### (Optional) Set up [Prettier](https://prettier.io/) and [ESLint](https://eslint.org/) in your IDE The Medplum CI system uses the tools [Prettier](https://prettier.io/) and [ESLint](https://eslint.org/) to enforce its code style and ensure consistent coding standards. If you are planning on submitting a PR, we highly suggest setting up these tools in your IDE to receive feedback and fix errors while developing. #### On Visual Studio Code The medplum repo comes with configured recommended Visual Studio Code extensions. When you first open with workspace, Visual Studio will automatically prompt you to install the recommended extensions. ![install recommended extensions](install-recommended-extensions.png) Alternatively, you can search for the "Recommended Extensions" in the Visual Studio extension manager and install from there. import videoUrl from './recommended-extensions.webm'; --- ## Working with package.json The Medplum codebase uses NPM workspaces to manage approximately 50 packages in a single monorepo. This structure provides many benefits but can be confusing when it comes to dependency management. This guide explains best practices for working with dependencies in our monorepo. ### Key Concepts #### Use the Pinned npm Version Use the npm version pinned in the `packageManager` field of the root `package.json`. Older versions of Node may come bundled with npm 10 or older, which may fail when rebuilding `package-lock.json` from scratch. ```bash # Check your npm version npm -v # Install the pinned version (run from the project root) npm install -g "$(node -p "require('./package.json').packageManager")" ``` #### Always Run npm Commands from the Root ```bash # ✅ DO run npm commands from the project root cd ~/dev/medplum npm ci # ❌ DON'T run npm commands from package directories cd ~/dev/medplum/packages/core npm i # DON'T do this! ``` Package dependency relationships are managed at the workspace level, and running npm commands in individual package directories can break the workspace structure. #### Prefer `npm ci` Over `npm i` ```bash # ✅ Preferred installation method npm ci ``` The `npm ci` (clean install) command: - Uses the exact versions in `package-lock.json` - Ensures you're using dependencies that have been tested in CI/CD - Deletes the existing `node_modules` folder to ensure clean installation - Is faster for fresh installs in most cases #### Use the Reinstall Script for Clean Installations ```bash # For a completely clean installation ./scripts/reinstall.sh ``` This script: 1. Recursively deletes all `node_modules` folders in the project 2. Runs `npm ci --strict-peer-deps` to ensure peer dependency conflicts are caught immediately #### Never Force Install Dependencies ```bash # ❌ DON'T use these commands npm install --force npm install --legacy-peer-deps ``` These flags bypass important dependency checks and can lead to unpredictable behavior, failed builds, and time-consuming debugging sessions. ### Adding New Dependencies Adding dependencies in a monorepo is different from a regular npm project: 1. **Don't** use `npm i [package-name]` directly 2. **Do** manually add the dependency to the appropriate package.json file: ```js // Example: Adding a dependency to packages/core/package.json { "dependencies": { "existing-dependency": "1.0.0", "new-dependency": "2.0.0" // Manually add this line } } ``` 3. Check if other packages use the same or similar dependency and try to align versions 4. After manually updating package.json, run from the project root: ```bash cd ~/dev/medplum npm i --strict-peer-deps --prefer-dedupe ``` This is the most surgical way update to `package-lock.json` Or use the reinstall script with the update flag: ```bash ./scripts/reinstall.sh --update ``` This will completely delete your `package-lock.json` file and all `node_modules` directories, and rebuild `package-lock.json` from scratch. Depending on the magnitude of your change, sometimes this can be the cleaner option. ### Package.json Best Practices - Always use exact version strings (not ranges): ```js // ✅ DO use exact versions "dependency": "1.2.3" // ❌ DON'T use version ranges "dependency": "^1.2.3" ``` - The exception is for peerDependencies, which should use broader ranges: ```js "peerDependencies": { "react": "^17.0.2 || ^18.0.0" } ``` - Keep dependencies in alphabetical order - Follow conventions from existing packages when creating new ones - Use our package.json formatter: ```bash npm run sort-package-json ``` > **Note:** Adding peer dependencies is an advanced topic. Please consult with the core engineering team before adding any peer dependencies to packages. ### Dependency Update Policy Medplum automatically upgrades dependencies every Monday: 1. GitHub Actions run on a cron timer (early Monday morning Pacific Time) 2. The process: - Creates a new git branch - Attempts minor upgrades using `npm-check-updates` - Installs updates with `./scripts/reinstall.sh --update` - Attempts major upgrades using `npm-check-updates` - Installs updates again Given this automatic process, we typically don't upgrade dependencies outside this schedule unless there's a critical security issue or must-have feature. ### Pull Request Checklist When preparing a pull request that involves dependency changes: - If you modified a `package.json` file, there should typically be corresponding changes in `package-lock.json` - If you didn't modify any `package.json` files, there likely shouldn't be changes to `package-lock.json` - There should only be one `package-lock.json` file (in the root directory) ### Common Troubleshooting #### Issue: "Unable to resolve dependency tree" Error This usually indicates a peer dependency conflict. Try: ```bash ./scripts/reinstall.sh ``` #### Issue: Local Changes to package-lock.json That You Didn't Intend This can happen if you accidentally ran `npm install` in a package directory. Reset with: ```bash git checkout -- package-lock.json ./scripts/reinstall.sh ``` #### Issue: Unexpected behavior after adding dependencies Ensure you've run the reinstall script from the root directory: ```bash ./scripts/reinstall.sh --update ``` ### Need Help? If you have questions about dependency management in the Medplum monorepo, please ask in: - Discord: [Medplum community server](https://discord.gg/medplum) - Slack: [#eng](https://medplum.slack.com/archives/C051SKJKZ50) channel --- ## Publishing NPM Packages This is the process we use to publish new versions of JavaScript NPM packages. Publishing NPM dependencies requires being a member of the [Medplum dev team](https://github.com/orgs/medplum/teams/dev). :::note[] Note that publishing to NPM is separate from deploying to production. For details on deploying to production, see: - Hosted Medplum uses CI/CD which automatically pushes to production on every merge to `main` - For self-hosted, see [Install on AWS - Ongoing](/docs/self-hosting/install-on-aws#ongoing) ::: ### Steps The Medplum publish process is automated using Github Actions. #### 1. Prepare the release - Go to [GitHub Actions](https://github.com/medplum/medplum/actions) - In the left side bar, click [Prepare release](https://github.com/medplum/medplum/actions/workflows/prepare-release.yml) - Click the "Run workflow" button - Use the default branch `main` - Click "Run workflow" This will initiate the action, which will create the release PR and a draft "Release". Wait for the normal PR process, and merge the PR when ready. #### 2. Publish the release - Go to [GitHub Releases](https://github.com/medplum/medplum/releases) - At the top, there should be a new release in "Draft" status - Click the pencil icon for "Edit" - Leave the tag alone - Update the "Target" - Click on the "Target: main" button - Go to the "Recent Commits" tab - Select the Release commit - Update the release notes as you see fit - Give shout-outs to new contributors - When necessary, add notes about new features - At your discretion, remove trivial commits - Click "Publish release" That will create a new Github "Release", which starts a "Publish" action. The publish action publishes all libraries to npm, and also builds the `medplum-agent-installer.exe` binary. --- ## Run the Stack Follow these instructions to get the complete Medplum stack running directly on your host machine. ### Prerequisites 1. **[Git](https://git-scm.com/)** 2. **[Node.js](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)** (version 22+ required) 3. **[Docker](https://docs.docker.com/engine/install/)** 4. [Clone the Medplum repo](./local-dev-setup#clone-the-repo) ### Install Our monorepo uses [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces), so installing dependencies for all projects is done using normal `npm install`: ```sh cd medplum npm ci ``` ### Build Use Turborepo to build all packages. From your root medplum directory run: ```sh npm run build:fast ``` ### Background services The Medplum Stack requires the following services to be running in your environment: - [PostgreSQL](https://www.postgresql.org/) for the primary database - [Redis](https://redis.com/) for caching and job queueing When running these services on your local machine you can either use Docker (recommended) or install them directly onto your machine. #### Using Docker (Recommended) Use the supplied `docker-compose.yml` file to run PostgreSQL and Redis background services. These services will be deployed with all necessary medplum configurations and database migrations. From your root `medplum` directory run ```sh docker-compose up ``` If this doesn't work for you, try ```sh docker compose up ``` This will: 1. Start the PostgreSQL server in a container 2. Set up the appropriate configurations (see [postgres.conf](https://github.com/medplum/medplum/blob/main/postgres/postgres.conf)) 3. Create databases (see [init_test.sql](https://github.com/medplum/medplum/blob/main/postgres/init_test.sql)) When `docker-compose` completes, you should see something like this in your terminal: ```bash postgres-1 | PostgreSQL init process complete; ready for start up. postgres-1 | postgres-1 | 2024-12-13 17:27:57.492 GMT [1] LOG: starting PostgreSQL 16.5 (Debian 16.5-1.pgdg120+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit postgres-1 | 2024-12-13 17:27:57.492 GMT [1] LOG: listening on IPv4 address "0.0.0.0", port 5432 postgres-1 | 2024-12-13 17:27:57.492 GMT [1] LOG: listening on IPv6 address "::", port 5432 postgres-1 | 2024-12-13 17:27:57.493 GMT [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" postgres-1 | 2024-12-13 17:27:57.495 GMT [67] LOG: database system was shut down at 2024-12-13 17:27:57 GMT postgres-1 | 2024-12-13 17:27:57.508 GMT [1] LOG: database system is ready to accept connections ``` **(Optional)** If you'd like to run a `psql` to shell to inspect the database, you can run ```sh docker exec -it medplum-postgres-1 psql -U medplum ``` Where `medplum-postgres-1` can be replaced with the name of your postgres docker container. #### Deploying manually If you'd prefer to install the dependencies directly, you can find installation instructions for the required services below: 1. [Install PostgreSQL](https://www.postgresql.org/download/) (version 13+ required) 2. [Install Redis](https://redis.io/download) (version 6+ required) After that, you will have to update the file `packages/server/medplum.config.json` ```js "database": { "host": "", // YOUR POSTGRESQL HOST "port": "", // YOUR POSTGRESQL PORT "dbname": "", // YOUR POSTGRESQL DB Name "username": "", // YOUR POSTGRESQL USERNAME "password": "" // YOUR POSTGRESQL PASSWORD }, "redis": { "host": "", // YOUR REDIS HOST "port": "", // YOUR REDIS PORT "password": "", // YOUR REDIS PASSWORD } ``` ### Run tests (optional) Use Turborepo to run all tests across all packages. From your root medplum directory run: ```sh npm t ``` ### Run #### Start the servers After you have PostgreSQL and Redis up and running, you can run the Medplum API server. From your root `medplum` directory run: ```sh cd packages/server npm run dev ``` This will seed the medplum database with an example project and user. The email and password for the example user are: | | | | ------------ | ----------------- | | **Email** | admin@example.com | | **Password** | medplum_admin | To make sure the server is working, you can access the health check at [http://localhost:8103/healthcheck](http://localhost:8103/healthcheck) If everything is working, you should see the following in your browser: ```json { "ok": true, "version": "", "postgres": true, "redis": true } ``` :::warning[Troubleshooting] If you get an error such as `error: role "medplum" does not exist` or `password authentication failed for user "medplum"` this usually means you have another instance of Postgres running. Closing the other instance should resolve this issue. ::: #### Run the Medplum Web App Lastly, you can start the Medplum Wep App. From your root `medplum` directory run: ```sh cd packages/app npm run dev ``` You can access the app at [http://localhost:3000/](http://localhost:3000/) The default username is admin@example.com, default password medplum_admin. --- ## Search Architecture :::caution[] This page documents internal Medplum implementation details, and refers to point-in-time code snapshots that may be different from the current application code, and are subject to change at any time without prior notice. ::: ### Resource Table Schema In addition to `id` and `content` columns containing the resource's UUID and raw JSON, each FHIR resource type table in the Medplum DB (e.g. `"Observation"`) contains columns corresponding to the search parameters for that resource type (e.g. `"Observation".subject`). The data types of these columns are mapped from the search parameter type: | FHIR SearchParameter type | Postgres data type | | ------------------------- | -------------------------------------------- | | `number` | `double precision` | | `date` | `date`/`timestamp with time zone` | | `string` | `text`\* | | `token` | `text`\*/`boolean` | | `reference` | `text` | | `composite` | - (unsupported) | | `quantity` | `double precision` | | `uri` | `text` | | `special` | - (e.g. `id uuid` and `compartments uuid[]`) | \* Some parameters are handled by special lookup tables due to complex matching semantics, see below Many of these columns can contain multiple values of the same type in an array, e.g. `text[]`. ### Parsing Search Queries FHIR search queries use the `application/x-fhir-query` MIME type, and generally follow the format `ResourceType?param1=value1¶m2=value2`, e.g. `Observation?status=final&subject:missing=true`. These [query strings are parsed][parse-query] into our own internal representation using helper functions in `@medplum/core` to facilitate common handling of search between the server and client-side applications. The resulting [`SearchRequest`][search-req] object contains all information necessary to process the search operation: ```ts interface SearchRequest { readonly resourceType: string; filters?: { code: string; operator: Operator; value: string; }[]; sortRules?: { code: string; descending?: boolean; }[]; offset?: number; count?: number; fields?: string[]; name?: string; total?: 'none' | 'estimate' | 'accurate'; include?: IncludeTarget[]; revInclude?: IncludeTarget[]; summary?: 'true' | 'text' | 'data'; } ``` Each search parameter from the query string corresponds to an element in `SearchRequest.filters`, and the example query above (`Observation?status=final&subject:missing=true`) would be parsed as: ```ts { resourceType: 'Observation', filters: [ { code: 'status', operator: Operator.EQUALS, value: 'final', }, { code: 'subject', operator: Operator.MISSING, value: 'true', } ] } ``` The [`Operator`][operator] enum represents a union over FHIR search [modifiers][search-modifiers] and [prefixes][search-prefixes]; combined, these define all logical search relations between parameter and value. Currently, there is zero overlap between parameters that use prefixes and those that allow modifiers; however, if this ever changes, modifications to this abstraction will be required. [parse-query]: https://github.com/medplum/medplum/blob/6ecbcf8fdc209dbf799ebe9df7d726217424f1f3/packages/core/src/search/search.ts#L172 [search-req]: https://github.com/medplum/medplum/blob/6ecbcf8fdc209dbf799ebe9df7d726217424f1f3/packages/core/src/search/search.ts#L7 [operator]: https://github.com/medplum/medplum/blob/6ecbcf8fdc209dbf799ebe9df7d726217424f1f3/packages/core/src/search/search.ts#L39 [search-modifiers]: http://hl7.org/fhir/R4/valueset-search-modifier-code.html [search-prefixes]: http://hl7.org/fhir/R4/search.html#prefix ### SQL Builder Medplum's core search logic translates FHIR search requests into a composable set of SQL expressions, from which the DB query is built. The [`SqlBuilder`][sql-builder] class provides basic functionality for constructing the SQL string, but most code uses it only indirectly through classes representing logical parts of the SQL expression, e.g. a [`Condition`][sql-condition]. Queries constructed this way are modular and flexible, allowing the server to easily manipulate the query without resorting to tricky string manipulation. The main search parameter processing logic takes places in [`buildSearchFilterExpression()`][build-search-filter-expr], which performs: 1. Checking if the search parameter is a special one that needs specific handling (e.g. [`_filter`][filter-param]) 2. Identifying whether a lookup table is in use for the search parameter and constructing the JOIN - The `:identifier` modifier is handled as a special case, using the token lookup table 3. Constructing the correct SQL `WHERE` expression for the search parameter based on its type and modifiers [sql-builder]: https://github.com/medplum/medplum/blob/6ecbcf8fdc209dbf799ebe9df7d726217424f1f3/packages/server/src/fhir/sql.ts#L213 [sql-condition]: https://github.com/medplum/medplum/blob/6ecbcf8fdc209dbf799ebe9df7d726217424f1f3/packages/server/src/fhir/sql.ts#L49 [build-search-filter-expr]: https://github.com/medplum/medplum/blob/6ecbcf8fdc209dbf799ebe9df7d726217424f1f3/packages/server/src/fhir/search.ts#L494 [filter-param]: http://hl7.org/fhir/R4/search_filter.html #### Lookup Tables Some search parameters require specific matching logic, and a separate lookup table to map parameter values to resources with acceptable performance. These lookup tables are modeled in subclasses of [`LookupTable`][lookup-table], and define custom logic for adding JOIN statements and conditions against the lookup tables to the SQL query. Each lookup table contains a `resourceId` column used for joining to the main resource tables, alongside other column used for matching. Some lookup tables are global across all resource types, where others are resource type-specific. For example, the schema of the [`HumanName`][human-name-lookup] lookup table is given below: | Column | Type | Nullable | | ---------- | ---------- | -------- | | id | `uuid` | not null | | resourceId | `uuid` | not null | | index | `integer` | not null | | content | `text` | not null | | name | `text` | | | given | `text` | | | family | `text` | | | name_tsv | `tsvector` | | | given_tsv | `tsvector` | | | family_tsv | `tsvector` | | [lookup-table]: https://github.com/medplum/medplum/blob/6ecbcf8fdc209dbf799ebe9df7d726217424f1f3/packages/server/src/fhir/lookups/lookuptable.ts [human-name-lookup]: https://github.com/medplum/medplum/blob/ecbfcbb0860e95ef3e7ea3ddc4508b9a18e55ca1/packages/server/src/fhir/lookups/humanname.ts #### Examples ##### Find Patient by ID (special search parameter for all resource types) ``` Patient?_id=c5a1e9bc-b627-4520-8a1c-cb6907a6e6c4 ``` ```sql -- Only retrieve necessary data from the associated DB table SELECT "Patient"."id", "Patient"."content" FROM "Patient" -- `deleted=false` filter added to all queries by default WHERE ("Patient"."deleted"=false AND "Patient"."id"='c5a1e9bc-b627-4520-8a1c-cb6907a6e6c4') ``` ##### Array column and boolean filters ``` Organization?_profile=http://hl7.org/fhir/us/core/StructureDefinition/us-core-organization &active=true ``` ```sql SELECT "Organization"."id", "Organization"."content" FROM "Organization" WHERE ("Organization"."deleted"=false AND ( -- && operator checks for overlap between arrays ("Organization"."_profile" IS NOT NULL AND "Organization"."_profile" && ARRAY[ 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-organization' ]::TEXT[]) -- Multiple search parameters are ANDed together in the SQL query AND "Organization"."active"=true )) ``` ##### Multiple values for single search parameter ``` Observation?_profile= http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab,http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-social-history ``` ```sql SELECT "Observation"."id", "Observation"."content" FROM "Observation" WHERE ("Observation"."deleted"=false AND ( -- && matches if arrays have any element in common "Observation"."_profile" IS NOT NULL AND "Observation"."_profile" && ARRAY[ 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-lab', 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-observation-social-history' ]::TEXT[] )) ``` ##### String matching in array column ``` Organization?name=GeneCo ``` ```sql SELECT "Organization"."id", "Organization"."content" FROM "Organization" WHERE ("Organization"."deleted"=false AND -- Subquery on array column is nearly (±5%) as performant as &&, -- provided that subquery is cheap EXISTS(SELECT 1 FROM unnest("name") AS "name" -- Strings use case-insensitive prefix matching WHERE LOWER("name") LIKE 'geneco%' LIMIT 1 ) ) ``` ##### Name lookup table ``` Practitioner?name=Dub ``` ```sql SELECT "Practitioner"."id", "Practitioner"."content" FROM "Practitioner" -- LEFT JOIN enables selecting matches or not -- JOINed tables are assigned numbered aliases LEFT JOIN "HumanName" AS "T1" ON ( "Practitioner"."id"="T1"."resourceId" AND -- Matching criteria are inserted into the JOIN ON -- tsv column is computed when resource is inserted into DB "T1"."name_tsv" @@ to_tsquery('simple','Dub:*') ) WHERE ("Practitioner"."deleted"=false AND -- Find rows that match the code "T1"."resourceId" IS NOT NULL -- GROUP BY is required with JOIN ) GROUP BY "Practitioner"."id" ``` ##### Token lookup table with modifier ``` DiagnosticReport?code:not=http://loinc.org|69737-5 ``` ```sql SELECT "DiagnosticReport"."id", "DiagnosticReport"."content" FROM "DiagnosticReport" LEFT JOIN "DiagnosticReport_Token" AS "T1" ON ( "DiagnosticReport"."id"="T1"."resourceId" AND -- Tokens are stored essentially as (searchParam, system, code) tuples "T1"."code"='code' AND ( "T1"."system"='http://loinc.org' AND "T1"."value"='69737-5' ) ) WHERE ("DiagnosticReport"."deleted"=false AND -- Negate the search and find rows that do NOT match "T1"."resourceId" IS NULL ) GROUP BY "DiagnosticReport"."id" ``` ### Permissions and Access Control Search results must obey the same permissions and access model as the rest of the server, so any resources the user does not have access to must not appear in the search results. Maintaining accurate paging through the results set requires that we do not filter anything out of the rows returned by the database: all security filters must [apply directly to the DB SQL query][add-project-filter]. These filters come in two kinds: one restricting the user to their own Project, and others related to associated AccessPolicy restrictions. The Project filter is added for all users other than Super Admins, and takes the form: ```sql -- Restrict to the Project compartment by its ID compartments && ARRAY['4410089e-6a88-4cc8-9cb6-3592ee18191c'] ``` AccessPolicy restrictions are specified using `application/x-fhir-query` expressions (e.g. `Observation?subject=Patient/c9d98310-d47d-4265-a377-4ec9317ceee6`), which are [parsed into SQL expressions][access-sql] the same way as the main search query being performed. Resources matching these criteria are allowed to be shown to the user with that AccessPolicy. These filter expressions are added to the SQL query, [wrapped in a `Disjunction`][access-disjunct] so any matching policy will grant access. [add-project-filter]: https://github.com/medplum/medplum/blob/6ecbcf8fdc209dbf799ebe9df7d726217424f1f3/packages/server/src/fhir/search.ts#L121 [access-sql]: https://github.com/medplum/medplum/blob/6ecbcf8fdc209dbf799ebe9df7d726217424f1f3/packages/server/src/fhir/repo.ts#L1006-L1012 [access-disjunct]: https://github.com/medplum/medplum/blob/6ecbcf8fdc209dbf799ebe9df7d726217424f1f3/packages/server/src/fhir/repo.ts#L1021 ### Search Indexing Search parameter values are [extracted from resources via FHIRPath expressions][search-value-extract] defined for each parameter. Most search parameter expressions are simple paths or variations thereof, but some parameters make use of more complex expressions to derive the search value: e.g. `Patient?deceased` (`Patient.deceased.exists() and Patient.deceased != false`). The results of evaluating the expression on the resource is [indexed into the database transactionally][db-index-tx] with the resource content, including any associated lookup tables. [search-value-extract]: https://github.com/medplum/medplum/blob/6ecbcf8fdc209dbf799ebe9df7d726217424f1f3/packages/server/src/fhir/repo.ts#L1116 [db-index-tx]: https://github.com/medplum/medplum/blob/6ecbcf8fdc209dbf799ebe9df7d726217424f1f3/packages/server/src/fhir/repo.ts#L615-L619 ### Appendix A: FHIR Search Concerns FHIR aims to provide a very broad and powerful search API, which places a significant burden on implementers to handle many different cases and cross-cutting concerns. A high-level summary of the entire scope of these concerns is given below: - **Context**: resource type, compartment, or system-level (with list of types or for all types) - **Parameter type**: number, date, string, token, reference, composite, quantity, uri, special - **Operators** - **Modifiers**: e.g. exact, missing, contains, identifier - **Prefixes**: eq, ne, gt, lt, ge, le, sa, eb, ap - **Chaining**: JOIN and filter on linked table columns, e.g. `Observation?subject.name=Fred&_has:DiagnosticReport:result:status=partial` - **Value collection**: Single value, comma-separated (OR), repeated parameter (AND) - FHIR search queries are logically in [CNF][cnf] - Parameters are ANDed together, and multiple values in one parameter are ORed - **Composite parameters**: used to incorporate [DNF][dnf] filters e.g. (paramA=x AND paramB=y) OR (paramA=u AND paramB=v) - **Sorting**: by search parameter, in forward or reverse order - **Pagination**: `_count` and `_offset` params, plus adding [paging links][paging-links] to the response - **Included resources**: `_include` and `_revinclude` params, include linked resources in the search response - **Subsetting**: `_summary` and `_elements` params, return only subsets of each resource for performance [cnf]: https://en.wikipedia.org/wiki/Conjunctive_normal_form [dnf]: https://en.wikipedia.org/wiki/Disjunctive_normal_form [paging-links]: http://hl7.org/fhir/http.html#paging --- ## Server Profiling Medplum is designed to be a critical component of system architecture. It is important that Medplum server is robust, performant, and scalable. Hand in hand with [Load Testing](/docs/self-hosting/load-testing), profiling the server is critical in understanding performance bottlenecks and scaling challenges. This document describes how the Medplum team uses the [Node.js Profiler](https://nodejs.org/en/docs/guides/simple-profiling) to analyze server performance. ### Prerequisites Make sure you first [Clone the repo](/docs/contributing/local-dev-setup#clone-the-repo) and [Run the stack](/docs/contributing/run-the-stack). ### Start the Profiler During normal server-side development, developers use `npm run dev` to run the server in development mode. That Change directories to `packages/server`: ```bash cd packages/server ``` Build the server: ```bash npm run build ``` Now run the server with the Node.js built-in profiler: ```bash NODE_ENV=production node --prof dist/index.js ``` If you use a custom config file, you can add that as an optional parameter: ```bash NODE_ENV=production node --prof dist/index.js file:medplum.docker.config.json ``` The server is now running with the profiler attached. Because we used `node` directly, the server will not automatically recompile or restart on changes. When the profiler is attached, you must manually rebuild and restart. ### Generate Load Generating load depends on which aspect of the server that you want to profile. For general guidance on using [Artillery](https://www.artillery.io/) to generate load, see the Medplum guide on [Load Testing](/docs/self-hosting/load-testing). For a quick test of a single HTTP endpoint, we can use Artillery's `quick` mode: ```bash artillery quick --count 20 --num 100 http://localhost:8103/healthcheck ``` Sample output: ``` All VUs finished. Total time: 3 seconds -------------------------------- Summary report @ 16:54:23(-0700) -------------------------------- http.codes.200: ................................................ 2000 http.request_rate: ............................................. 1702/sec http.requests: ................................................. 2000 http.response_time: min: ......................................................... 1 max: ......................................................... 19 median: ...................................................... 3 p95: ......................................................... 5 p99: ......................................................... 6 http.responses: ................................................ 2000 vusers.completed: .............................................. 20 vusers.created: ................................................ 20 vusers.created_by_name.0: ...................................... 20 vusers.failed: ................................................. 0 vusers.session_length: min: ......................................................... 237.9 max: ......................................................... 355.9 median: ...................................................... 333.7 p95: ......................................................... 354.3 p99: ......................................................... 354.3 ``` ### Analyze Profiler Data Switch back to your server, and terminate the process with Ctrl+C. Since we ran our application using the `--prof` option, a tick file was generated in the same directory as your local run of the application. It should have the form `isolate-0xnnnnnnnnnnnn-v8.log`. In order to make sense of this file, we need to use the tick processor bundled with the Node.js binary. To run the processor, use the `--prof-process` flag: ```bash node --prof-process isolate-0xnnnnnnnnnnnn-v8.log > processed.txt ``` Open `processed.txt` in your favorite text editor. If you have worked with profiler data before, this should look familiar. See the Node.js [Profiling](https://nodejs.org/en/docs/guides/simple-profiling) docs for full details on how to process and analyze that file. --- ## Terminology Architecture :::caution[] This page documents internal Medplum implementation details, and refers to point-in-time code snapshots that may be different from the current application code, and are subject to change at any time without prior notice. ::: ### Table Schema Medplum uses a handful of dedicated tables to store terminology data. The foundation of these is `Coding`, which stores individual codes related to `CodeSystem` resources by their ID: **Coding** | Column | Type | Nullable | Notes | | ------- | -------- | -------- | -------------------- | | id | `bigint` | not null | Primary key | | system | `uuid` | not null | Refers to CodeSystem | | code | `text` | not null | | | display | `text` | | | The `Coding` table has a unique index on `(system, code)` — there can only be one row for a given code per CodeSystem. Codings can also have properties, which define additional metadata about the codes. These are defined in the `CodeSystem_Property` table, and then referenced in `Coding_Property` alongside the property values: **CodeSystem_Property** | Column | Type | Nullable | Notes | | ----------- | -------- | -------- | ----------------------------------------------------------------- | | id | `bigint` | not null | Primary key | | system | `uuid` | not null | Refers to CodeSystem | | code | `text` | not null | Name of the property | | type | `text` | not null | Type of value the property takes | | uri | `text` | | URI describing any special roles the property has (e.g. `parent`) | | description | `text` | | | The `CodeSystem_Property` table has a unique index on `(system, code)` — there can only be a single property for a given code per CodeSystem. **Coding_Property** | Column | Type | Nullable | Notes | | -------- | -------- | -------- | ----------------------------- | | coding | `bigint` | not null | Refers to Coding | | property | `bigint` | not null | Refers to CodeSystem_Property | | target | `bigint` | | Refers to Coding | | value | `text` | | | There is a covering unique index on `Coding_Property`; each row must be fully unique. It is valid for a Coding to have multiple values for a given property. ### CodeSystem Hierarchies Many code systems define parent-child relationships between their codes, to denote that one code is a more general category and the other a more specific instance of that category. These relationships are stored as properties of the related codes: the properties are marked with a special `uri` (e.g. `http://hl7.org/fhir/concept-properties#parent` or `http://hl7.org/fhir/concept-properties#child`), and the `target` of the `Coding_Property` row points to the other Coding. For example, two codes might be related as follows: ```mermaid flowchart LR c1(["`Coding id: 1 code: '78012-2' `"]) cs(["`CodeSystem id: 434f1042-6962... url: 'http://loinc.org' `"]) c2(["`Coding id: 2 code: 'LP376020-6' `"]) csp(["`CodeSystem_Property id: 12 code: 'parent' uri: 'http://...#parent' `"]) cp([Coding_Property]) c1 -->|system| cs c2 -->|system| cs csp -->|system| cs cp -->|coding| c1 cp -->|property| csp cp -->|target| c2 ``` :::note[] While FHIR supports specifying hierarchical relationships with either `parent` or `child` properties, Medplum currently only supports `parent` for simplicity, since the two are inversely equivalent. ::: ### Terminology Operations FHIR specifies a suite of Operation endpoints to interact with terminology information, which Medplum implements on top of the tables described above. #### `CodeSystem/$validate-code` Validating whether a CodeSystem contains any of a set of codes is a simple query: ```sql -- Get CodeSystem by URL SELECT id, content FROM "CodeSystem" WHERE url = ?; -- Check whether codes exist SELECT id, code, display FROM "Coding" WHERE code IN (?, ?) AND system = ?; ``` #### `CodeSystem/$lookup` Looking up a given code in a CodeSystem is similar to the `$validate-code` operation above, but also looks up any properties of the given code: ```sql -- Get CodeSystem by URL SELECT id, content FROM "CodeSystem" WHERE url = ?; -- Look up code and attached properties SELECT "Coding".display, property.code, property.type, property.description, property.value FROM "Coding" LEFT JOIN "Coding_Property" AS cp ON "Coding".id = cp.coding LEFT JOIN "CodeSystem_Property" AS property ON cp.property = property.id WHERE "Coding".code = ? AND "Coding".system = ?; ``` #### `CodeSystem/$subsumes` Subsumption testing involves checking whether one code is related to another through a hierarchy defined by the code system. For example, one might want to check whether the code for "amoxicillin" descends from the code for "antibiotics". The `parent` properties from each code are recursively traversed to check if either is an ancestor of the other: ```sql -- Get CodeSystem by URL SELECT id, content FROM "CodeSystem" WHERE url = ?; -- Check if codeA is an ancestor of codeB; this query is performed once in each direction WITH RECURSIVE "cte_ancestors" AS ( SELECT id, code, display FROM "Coding" WHERE system = ? AND code = ? UNION SELECT c.id, c.code, c.display FROM "Coding" c INNER JOIN "Coding_Property" AS cp ON c.id = cp.target INNER JOIN "CodeSystem_Property" AS property ON ( cp.property = property.id AND property.code = ? ) INNER JOIN "cte_ancestors" AS ancestor ON cp.coding = ancestor.id WHERE c.system = ? ) SELECT code, display FROM "cte_ancestors" WHERE code = ? LIMIT 1; ``` #### `ValueSet/$validate-code` FHIR ValueSets [define which codes should be used](/docs/terminology#defining-and-using-code-systems) from specific code systems for some use case. Determining if a given code is in the ValueSet is one of the core FHIR terminology operations, and is designed to be used in resource validation. For each `ValueSet.compose.include` that matches the `system` of the coding to be validated, the server checks if the given code satisfies the inclusion criteria. If there is `filter`, the database is queried to check if the required properties are present. For hierarchical `is-a` filters, the above recursive ancestor query is used; otherwise, a generic property filter is applied: ```sql SELECT c.id, c.code, c.display FROM "Coding" c LEFT JOIN "Coding_Property" AS cp ON ( c.id = cp.coding AND cp.value = ? ) LEFT JOIN "CodeSystem_Property" AS property ON ( c.system = property.system AND property.id = cp.property AND property.code = ? ) WHERE ( c.system = ? AND c.code = ? AND cp.value IS NOT NULL AND property.system IS NOT NULL ); ``` #### `ValueSet/$expand` ValueSet expansion is similar to the `$validate-code` operation, but rather than checking a single code it returns an (optionally-filtered) list of codes that are contained in the ValueSet. This is often used to support typeahead queries in user interfaces. For each `ValueSet.compose.include`, codes from the given system are added to the expansion. If a `filter` parameter is passed to the `$expand` operation, this is used to constraint the returned set of codes: ```sql -- Expand a hierarchy, starting from the parent code WITH RECURSIVE "cte_descendants" AS ( SELECT id, code, display FROM "Coding" WHERE system = ? AND code = ? UNION SELECT c.id, c.code, c.display FROM "Coding" c INNER JOIN "Coding_Property" AS cp ON c.id = cp.coding AND cp.property = ? INNER JOIN "cte_descendants" AS descendant ON cp.target = descendant.id ) SELECT id, code, display FROM "cte_descendants" LIMIT 101; -- When a text filter is present, apply it first -- and then check whether those codes are in the hierarchy SELECT id, code, display FROM "Coding" WHERE ( system = ? AND to_tsvector('english', display) @@ to_tsquery('english', ?) AND EXISTS( WITH RECURSIVE "cte_ancestors" AS ( SELECT origin.id, origin.code, origin.display FROM "Coding" origin WHERE origin.system = ? AND origin.code = "Coding".code UNION SELECT c.id, c.code, c.display FROM "Coding" c INNER JOIN "Coding_Property" AS cp ON c.id = cp.target AND cp.property = ? INNER JOIN "cte_ancestors" AS ancestor ON cp.coding = ancestor.id ) SELECT 1 FROM "cte_ancestors" WHERE code = ? ) ) LIMIT 21; ``` The `filter` parameter matches against both the `display` and the `code` of each concept. Display matching is a case-insensitive substring match, backed by the `(system, display gin_trgm_ops)` trigram index. Code matching is a **case-insensitive prefix** match (`LOWER(code) LIKE 'filter%'`) for filters of at least three characters, falling back to exact code equality for shorter filters; the prefix match is served as a bounded btree range scan by the `(system, lower(code) text_pattern_ops)` index. Restricting code matching to a prefix (rather than an unanchored substring) keeps the candidate set small and index-driven, which is what preserves `$expand` performance when a typeahead filter is combined with an `is-a`/`descendent-of` hierarchy over a large CodeSystem. The code branch matches **canonical rows only** (`synonymOf IS NULL`): synonym rows share their canonical row's `code`, so they are redundant for code matching. Accordingly the supporting index is **partial** (`WHERE "synonymOf" IS NULL`), mirroring the primary `(system, code)` index — this keeps it smaller and further trims the candidate set fed to the hierarchy walk. The display branch is deliberately not restricted this way, so alternate terms carried on synonym rows remain searchable. #### `CodeSystem/$import` Medplum supports a [non-standard Operation][import-operation] to load codes into the database for large code systems. It generates bulk inserts for codes and their properties: ```sql -- Get CodeSystem by URL SELECT id, content FROM "CodeSystem" WHERE url = ?; -- Import codes INSERT INTO "Coding" (system, code, display) VALUES (?, '8867-4', 'Heart rate'), (?, '8302-2', 'Body height') ON CONFLICT (system, code) DO UPDATE SET display = EXCLUDED.display; -- Get property by system and code SELECT id FROM "CodeSystem_Property" WHERE system = ? AND code = ?; -- Import properties INSERT INTO "Coding_Property" (coding, property, value, target) VALUES (1, 12, 'LP415671-9', 2), (1, 21, 'Qn', null) ON CONFLICT DO NOTHING; ``` [import-operation]: https://github.com/medplum/medplum/blob/78cf54ff8bc2fbc3bfecb0c73be1c581179fae2d/packages/server/src/fhir/operations/codesystemimport.ts#L11-L38 --- ## Testing Medplum strongly believes in the importance of testing. We use the following tools for testing: - [Jest](https://jestjs.io/) as the primary test runner - [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) for React testing - [@testing-library/dom](https://www.npmjs.com/package/@testing-library/dom) - DOM testing utilities - [@testing-library/jest-dom](https://www.npmjs.com/package/@testing-library/jest-dom) - Jest matchers for DOM state - [@testing-library/react](https://www.npmjs.com/package/@testing-library/react) - Ties it all together: Jest, React, and DOM Every pull request is analyzed by [Sonarcloud](https://sonarcloud.io/project/overview?id=medplum_medplum) and [Coveralls](https://coveralls.io/github/medplum/medplum?branch=main) for code coverage and other static analysis. ### How to test To run all tests for all packages, use the build script: ```bash npm t ``` To run all tests for a single package, use `npm t` inside the package folder: ```bash cd packages/app npm t ``` To run tests for a single file, pass in the file name: ```bash npm t -- src/App.test.tsx ``` To run a single test in a single file, pass the file name and the test name: ```bash npm t -- src/App.test.tsx -t 'Click logo' ``` Any time you run `npm t`, you can optionally pass in `--coverage` to collect code coverage stats. They will be printed in the terminal: ```bash npm t -- --coverage npm t -- src/App.test.tsx --coverage npm t -- src/App.test.tsx -t 'Click logo' --coverage ``` --- ## Upgrading Dependencies Medplum upgrades dependencies regularly to ensure we have the latest security patches and bug fixes. This document describes the process for upgrading dependencies. Every Monday at 9:00 AM UTC, the "Upgrade Dependencies" Github Action runs automatically (see `.github/workflows/upgrade-dependencies.yml`). This action does the following: 1. Runs the `upgrade.sh` script, which updates all dependencies to the latest versions. 2. Runs the `reinstall.sh` script, which reinstalls all dependencies. 3. Creates a new branch and opens a pull request. ### Exceptions This is a list of dependencies that have known issues with automated upgrades. #### node-fetch At the time of this writing, we use node-fetch version 2.7.0. The developers of node-fetch have admirably taken the position that ESM-only libs are the future. The version 3 series is ESM-only. Unfortunately, our current server configuration is not yet compatible with ESM-only modules (despite many attempts). Therefore, for now, we keep node-fetch pinned at the latest version in the version 2 series, which continues to receive security fixes. In the future, there are 3 possibilities: - As the ESM ecosystem matures, we eventually support an ESM-only dependency. - Move to built-in `fetch` (available in supported Node.js LTS releases) ([#1563](https://github.com/medplum/medplum/issues/1563)) - For completely unrelated reasons (i.e., upload progress monitoring), we may move to [Axios](https://www.npmjs.com/package/axios), which would eliminate this exception entirely. --- ## Access Control Decision Guide Use this guide to define who can access which data and how organizational boundaries are enforced in Medplum. Work through tenancy, patient and practitioner sharing, role permissions, and administration to produce a membership and access-policy model. Use the [Authentication Decision Guide](/docs/decision-guides/authentication) for login and identity-provider choices. _Companion to the [Authorization and Access Control](/docs/access) docs._ Plan access control around four choices: 1. **[Define data separation and sharing](#data-separation-and-sharing)** if organizations, locations, teams, or service lines need isolation. Choose tenant boundaries, shared resources, and how enrollment or transfers change access. 2. **[Choose each role's data scope](#role-data-scope).** For multiple tenants, decide whether access is combined across tenants or separated by authenticated session. 3. **[Define role permissions](#role-permissions).** Choose resource types and interactions, then add field restrictions, write constraints, or attribute filters where needed. 4. **[Assign administration](#administration).** Decide who manages users, memberships, and configuration, including the clinical access those administrators need. Also plan [patient and caregiver access](#patient-and-caregiver-access) if you offer a patient portal, [third-party app access](#third-party-app-access) if you need SMART launch or delegated access, and [network restrictions](#network-restrictions) if IP-based controls are required. Use the [questions below](#before-you-start) to identify your users and organizational structure, then distinguish API restrictions from UI filtering before choosing the relevant access controls. Add requirements beyond these examples where needed. ### Before you start #### Who needs access? - Patients / caregivers only - Providers / staff / partners (clients or outside vendors) only - Both #### How is your organization structured? - Single organization - Multiple locations of one organization - Multiple independent organizations (MSO-style) - Departments or service lines within one organization - Combination #### Does this restriction need to prevent API access, or only change what the UI displays? ##### Option A: Prevent access to the data The user must not retrieve or modify the data, including through direct API requests. Enforce the restriction through `AccessPolicy` and `ProjectMembership` configuration. ##### Option B: Change the displayed view The user is authorized to access the data, but the application should show a narrower view. Use UI filters or display settings within the user's permitted access. For example, a clinician authorized for three clinics can view one clinic at a time using a UI selector. If each session must only access one clinic's data, its authenticated permissions must also be limited to that clinic. **Both can apply:** the API defines the permitted access; the UI selects what to display within it. Apply this distinction to tenant boundaries, resource permissions, and individual fields throughout the guide. ### Data separation and sharing **For applications that need isolation between organizations, locations, teams, or service lines.** If you have a single organization with no internal isolation, skip to [Role data scope](#role-data-scope). Within a project, tenant isolation uses **compartments** matched by parameterized access policies. Use `$set-accounts` to assign tenant references; Medplum computes `meta.compartment`. These tenant assignments extend the standard FHIR patient-compartment model. **Recommended pattern:** Assign patients to tenants through enrollment workflows, then grant practitioners access through parameterized `ProjectMembership.access` entries. See [Multi-Tenant Access Control](/docs/access/multi-tenant-access-policy). #### What does a tenant represent? Choose what a tenant represents in your application. What do you call these groups: clinics, programs, sites, or accounts? ##### Option A: Organizations, clinics, or practices Use `Organization` when tenants represent clinics, practices, or healthcare organizations in an MSO model. ##### Option B: Service lines or departments Use `HealthcareService` to represent functional groups within an organization. ##### Option C: A dedicated team for each patient Use a `CareTeam` per patient. ##### If tenants form a hierarchy: how will access extend to child tenants? One approach for nested tenants, such as region → clinic, is to grant access through the lowest-level tenants: - Patients are tagged only at the **lowest level** they belong to. - Practitioners enrolled at an intermediary level get one `ProjectMembership.access` entry per lowest-level tenant under it (iteratively expanded). - Adding a new lowest-level tenant requires a workflow to update every practitioner membership at or above that intermediary. #### Can a patient belong to one tenant or several? | Patient access | Approach | | :---------------------------- | :----------------------------------------- | | One tenant per patient | One tenant assignment on the Patient | | Patient shared across tenants | Multiple tenant assignments on the Patient | #### Are practitioners visible across tenants or only within their own? | Practitioner visibility | Approach | | :--------------------------------------- | :------------------------------------------------------------ | | Shared across tenants (global directory) | Allow `Practitioner` reads without a tenant filter | | Visible only within their tenant | Tag with tenant compartment; policy filters by `_compartment` | #### Which resources are shared across tenants? For terminology (`CodeSystem`, `ValueSet`), Questionnaires, PlanDefinitions, and ActivityDefinitions, decide whether resources are shared or tenant-specific. Also identify any resources shared across projects. | Situation | Approach | | :------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | | All resources of a type are shared | Allow reads without a tenant filter for that type | | Tenant-specific Questionnaires | Use `$set-accounts` to assign the tenant; filter by `_compartment` | | Some resources of a type are shared | Use separate criteria for authorized tenant resources and explicitly designated shared resources. An unfiltered rule would also expose other tenants' resources | | Bots used across tenants | Configure access to the Bot separately from its execution permissions; by default it uses its own `ProjectMembership` policy | | Reference data shared across projects | A super admin configures a linked project and its exported resource types; access through the link is read-only and still subject to policy | Allow the terminology resources your UI needs; overly restrictive policies can leave dropdowns empty. For Bots, see [Run as User](/docs/bots/bot-run-as-user); for shared reference data, see [Linked Projects](/docs/access/projects#project-linking). #### How will patients be enrolled or transferred between tenants? **Questions:** - When does a patient get enrolled in a tenant? (registration, first appointment, referral, insurance, geography) - Who is authorized to move a patient between tenants? - On transfer: do records follow the patient, stay with the original tenant, or become shared? | Situation | Approach | | :----------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Patient enrollment | An authorized admin or Bot calls [`$set-accounts`](/docs/api/fhir/operations/set-accounts) with the full tenant list; use `propagate: true` to update existing patient-compartment resources | | Transfer all patient-compartment data | Replace the old tenant in the list and propagate the change. Verify completion and any other grants before treating access as revoked | | Share data with both tenants | Include both tenants in the list and propagate the change | | Keep historical records with the original tenant | Design a separate record-level assignment workflow; patient-wide propagation does not distinguish historical from future records | `$set-accounts` requires project-admin or super-admin authority and checks access to the affected resources. Propagation follows each resource's own [patient-compartment references](/docs/access/multi-tenant-access-policy#why-is-patient-a-special-case), not arbitrary links between resources. **Reference implementation:** [MSO demo `enrollment.ts`](https://github.com/medplum/medplum/blob/main/examples/medplum-mso-demo/src/utils/enrollment.ts). **Next:** Define each role's data scope in [Role data scope](#role-data-scope). ### Role data scope List your roles, such as physician, RN, MA, front desk, billing, care coordinator, and medical director. **If you have multiple tenants, choose the data scope for each role below.** Otherwise, continue to [Role permissions](#role-permissions). #### Option A: Access to one tenant Use a single `ProjectMembership.access` entry parameterized with that tenant. #### Option B: Combined access to several tenants Use one `access` entry per tenant with the same policy. #### Option C: Organization-wide or cross-tenant oversight Use an unparameterized policy with no `_compartment` filter. #### Option D: Separate authenticated sessions for each tenant Give each session a membership limited to one tenant. Switching context means switching the authenticated login, rather than changing a UI filter. Separate credentials are not always necessary: the [Invite API](/docs/api/project-admin/invite) supports creating additional memberships with `forceNewMembership`. #### How do these policies combine? **Policy grants are additive, not a way to subtract access.** Do not add a restrictive policy to narrow an existing broad grant. For overlapping resource rules, the first matching rule determines field restrictions and write constraints; design and test those rules together. Implementation: [Parameterized Policies](/docs/access/access-policies#parameterized-policies), [Invite API](/docs/api/project-admin/invite). **Next:** Choose what each role can do with that data in [Role permissions](#role-permissions). ### Role permissions #### Which resource types and interactions does the role need? For each role, list the resource types it needs and choose which interactions it can perform, such as reading, creating, updating, searching, or deleting. | Permission option | Approach | | :---------------------------------------------------------------- | :------------------------------------------------------ | | All supported FHIR interactions | Standard `resource` entry, no `interaction` restriction | | Read-only | `readonly: true` | | Selected interactions, such as create-only or read without search | Explicit `interaction` array | #### Does the role need additional restrictions? **Apply only the restrictions your roles need.** ##### Should any fields be hidden or read-only? Use field-level restrictions for specific elements, such as a Patient's address. | Situation | Approach | | :------------------------------------ | :------------------------------------------------------------------------------ | | Readable but not writable | `readonlyFields` | | Hidden from returned resources | `hiddenFields`; these restrictions also affect writes, not just returned output | | No access to a clinical resource type | Omit that type from the role's grants; ensure no broader rule grants access | Field masking is not a substitute for denying access to a resource. For presentation-only changes, hide fields in the UI. ##### Should writes enforce required fields or state transitions? Use write constraints to enforce these rules at the policy layer: | Situation | Approach | | :---------------------------------------------- | :------------------------------------------------------------------------------------ | | Prevent a field changing after a terminal state | Use `writeConstraint` to check the prior state and compare the field before and after | | Require a field when entering a state | Check `%after` and field presence in a `writeConstraint` | | Apply a constraint only to updates | Prefix with `%before.exists() implies ...` | | Apply a constraint only to creates | Prefix with `%before.exists().not() implies ...` | These prefixes control when a constraint applies; use `interaction` to allow only creates or updates. Keep FHIRPath simple. If a Bot enforces complex rules, use a controlled invocation path; a Subscription-triggered Bot runs after the write and cannot reject it. ##### Should access depend on resource attributes? Use criteria-based filtering for attributes such as geography, code, or status: | Situation | Approach | | :------------------------- | :-------------------------------------------- | | Patient address filtering | `Patient?address-state=%state`, parameterized | | Several allowed states | One `access` entry per state | | Specialty or status filter | `criteria` with code/status filter | | Criteria \+ tenant | Single `criteria` string combining both | Address filtering restricts records; it does not validate practitioner licensure. `criteria` supports a subset of FHIR search, with `:not` and `:missing` modifiers and no chained searches. Denormalize or assign tenant references through an enrollment workflow where needed. **Files:** `Binary` resources do not support policy `criteria`; set an appropriate `securityContext`. See [Binary Security Context](/docs/access/binary-security-context). Implementation: [Access Policies](/docs/access/access-policies), [Licensure and Scheduling](/docs/scheduling/state-by-state-licensure). #### Should the UI change based on the role? **If your frontend shows or hides UI based on the user’s role.** | Situation | Approach | | :----------------------- | :--------------------------------------------------------------- | | UI changes based on role | Read the effective policy's `basedOn` references from `/auth/me` | `basedOn` identifies the policies used to build the effective policy; it is not a policy-inheritance setting. Use it to inform the UI, while continuing to enforce access at the API. **Next:** Assign administrative responsibilities in [Administration](#administration). ### Administration Decide who manages users and memberships, who manages Bots, ClientApplications, and AccessPolicies, and whether those administrators should see clinical data. Identify any server-level needs, such as project creation or overwriting protected fields. | Situation | Approach | | :------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------- | | Trusted project administrator | `ProjectMembership.admin` grants project administration capabilities | | Admin with limited routine clinical access | Assign an explicit custom AccessPolicy limiting clinical resources | | A role that must not access clinical content | Use a non-admin role with narrowly scoped permissions; do not rely on `hiddenFields` or an admin's clinical policy as a hard boundary | | Project creation or protected server-level changes | Super-admin operations; for hosted Medplum, coordinate with Medplum | | Separate user-management and technical duties | Define the permitted operations and use controlled backend workflows where needed; stacking policies does not subdivide the admin flag | **Project admins are trusted administrators.** They can manage access policies and memberships, and act on behalf of users. A clinical policy limits their routine access but does not prevent them from changing grants. The admin flag does not bypass clinical policies; omitting a policy can still grant broad access. **Default or custom policy?** The built-in Admin default grants broad read/write access. When admin status changes, Medplum swaps the recognized Practitioner/Admin default to the matching default, if configured. Custom policy assignments are unchanged by this swap; the admin flag still changes administrative capabilities. Use an explicit custom policy when the defaults are too broad. Implementation: [Admin Users](/docs/access/admin), [Default Access Policies](/docs/access/access-policies#default-access-policies), [On-Behalf-Of](/docs/auth/on-behalf-of). **Next:** Review the additional decisions below that apply to your application. ### Patient and caregiver access **For applications with a patient portal.** Choose how patients register, whether caregivers or proxies need access (such as parents, adult children, or guardians), and how those relationships are established and revoked. #### How will patients register? | Registration option | Approach | | :--------------------- | :----------------------------------------------------------------------- | | Invite-only | Use the Invite API with a patient-scoped policy | | Open self-registration | Set `Project.defaultPatientAccessPolicy`; this enables open registration | **Check both enrollment paths.** Open registration uses `Project.defaultPatientAccessPolicy`. Patient invitations without an explicit policy prefer the Patient entry in `Project.defaultAccessPolicies`, falling back to `defaultPatientAccessPolicy` if that entry is absent. Align these defaults if both paths should grant the same access. Changing the registration default affects future registrations; it does not migrate existing memberships. See [Default Access Policies](/docs/access/access-policies#default-access-policies). #### Which patient records can each person access? | Access option | Approach | | :----------------------------------------------------- | :--------------------------------------------------------------------------------------------------------- | | Patient sees own data | Parameterized policy with `%patient` on clinical resources | | Single caregiver, single patient | One `access` entry with `%patient` | | One caregiver, multiple patients | Multiple `access` entries, different `%patient` | | Multiple caregivers per patient | Each caregiver has their own ProjectMembership | | Revoke caregiver | Remove the corresponding grants and verify API access is revoked | | Time-bound caregiver access (e.g., expires at age 18\) | Automate removal of the corresponding membership grants at expiry; hiding access in the UI is insufficient | Implementation: [Patient and Caregiver Policies](/docs/access/access-policies#caregiver-access), [Open Patient Registration](/docs/user-management/open-patient-registration). ### Third-party app access **If an app needs SMART launch or delegated access.** SMART is commonly used for third-party integrations, but is not limited to them. Choose patient-context, provider-context, or standalone launch. Define the scopes the app needs and whether it needs offline access. | Situation | Approach | | :-------------------------------------------------- | :----------------------------------------------------------------------------------- | | Your own frontend without SMART launch requirements | Use a standard ClientApplication and AccessPolicy | | App launched from an EHR or portal | Request `launch` plus the clinical data scopes the app needs | | Standalone app needing patient selection | Request `launch/patient` plus patient data scopes, such as `patient/*.rs` | | Patient data access | Use `patient/` scopes with patient context; narrow the resource types where possible | | Access within the current user's permissions | Use `user/` scopes and the user's AccessPolicy | | Access after the user is offline | Request `offline_access` for a refresh token | Launch scopes request context; they do not grant clinical data access. Clinical scopes work alongside AccessPolicy restrictions. Use `.rs` for read/search access or `.cruds` for full access within the policy; enforce finer interaction restrictions through `AccessPolicy.resource.interaction` rather than assuming arbitrary SMART permission-letter combinations are enforced. Implementation: [SMART Scopes](/docs/access/smart-scopes). ### Network restrictions **If access should be restricted by IP range.** Choose which networks can connect, such as a VPN or clinic network, and whether different roles need different rules. | Situation | Approach | | :----------- | :------------------------------------- | | VPN-only | Allow VPN ranges \+ wildcard block | | On-prem only | Allow clinic network \+ wildcard block | | Mixed | Different AccessPolicies per role | Use the documented IPv4 address or address-prefix rules, not CIDR notation. The first matching rule wins; end an allowlist with a wildcard block. These rules apply to supported authentication paths, but are not enforced on refresh-token requests or rechecked on every bearer-token API call. Do not use them as the sole control for a requirement that every request originate from an approved network. Implementation: [IP Access Rules](/docs/access/ip-access-rules). --- ## Authentication Decision Guide Use this guide to choose how users sign in to your application and how it authenticates requests to Medplum. Compare the options and tradeoffs, then follow the linked technical docs to implement your choices. _Companion to the [Authentication](/docs/auth) and [User Management](/docs/user-management) docs. Builds on [Identity Management: A Practical Guide](/blog/identity-management)._ Staff, partner clinicians, patients, and caregivers may need different approaches. For each population: 1. **[Choose your identity provider](#decision-1-which-identity-provider-will-you-use).** Use Medplum-managed authentication or an external IDP. 2. **[Choose your token flow](#decision-2-what-is-your-token-flow-architecture)** if you use external or existing authentication. 3. **[Choose your matching key](#decision-3-how-will-you-match-external-identities-to-medplum-users)** if you use an external IDP authorization-code flow or token exchange. All approaches need [user provisioning and session lifecycle decisions](#before-you-implement). For tenancy and permissions, use the [Access Control Decision Guide](/docs/decision-guides/access-control). --- ### Decision 1: Which identity provider will you use? An identity provider (IDP) manages credentials and authenticates users. Choose based on your existing identity systems and SSO requirements. #### Option A: Medplum-managed authentication Use Medplum's built-in email/password login or Google sign-in. For password-based accounts, Medplum manages credentials, registration, password resets, and MFA. - **Advantages:** - Built-in authentication and login components without a separate IDP integration. - **Tradeoffs:** - Does not meet a requirement to use an existing corporate IDP. - **Best fit:** Applications without an existing identity system, including patient-facing applications. Implementation: [Redirect login](/docs/auth/medplum-as-idp), [Embedded Login](/docs/auth/embedded-login), and [Google sign-in](/docs/auth/google-auth). **Next:** Skip Decisions 2 and 3 and continue to [Before You Implement](#before-you-implement). #### Option B: External IDP with application-based routing Configure an external IDP on the Medplum `ClientApplication`. For example, your patient portal and clinician application can use different providers. - **Advantages:** - Reuses existing identities and advanced features from providers such as Okta or Auth0. - Different applications can use different identity providers. - **Tradeoffs:** - Multiple upstream IDPs within one application need additional routing, such as an identity broker. - **Best fit:** Each application selects a known IDP or broker for its users. Implementation: [External Identity Providers](/docs/auth/external-identity-providers). #### Option C: External IDP with domain-based routing Medplum selects the IDP from the user's email domain, such as routing `user@clinic-a.com` to Clinic A's corporate IDP. - **Advantages:** - An organization's SSO applies across Medplum applications, including the Medplum App. - **Tradeoffs:** - Applies to all users of the domain, including developer accounts. - Requires Medplum setup support for both hosted and self-hosted deployments. Hosted setup requires Enterprise. - **Best fit:** Customer organizations bring their own IDPs and their email domains should determine where users sign in. Implementation: [Domain-level Identity Providers](/docs/auth/domain-level-identity-providers). **Next for external IDPs:** Choose your token flow below. ### Decision 2: What is your token flow architecture? **For applications using external or existing authentication.** Choose how the application obtains Medplum credentials. Here, "client" means your browser or native app; "server" means your application backend. Both the authorization-code flow and token exchange can use the same external IDP. The difference is whether Medplum handles the IDP callback or your application obtains an external access token and exchanges it. #### Option A: External IDP authorization-code flow (three-legged OAuth) Medplum handles the external IDP callback and returns an authorization code to your application, which exchanges it for Medplum tokens. - **Advantages:** - The client can call Medplum directly using the SDK and authenticated components. - The client does not need to retain an external access token for this flow. - **Tradeoffs:** - Requires redirect login and coordinated callback settings. - Does not provide the client with an external access token for other services. - **Best fit:** Medplum is the primary client API and redirect login fits your application. Start here unless another flow meets a specific requirement. Implementation: [External Identity Providers](/docs/auth/external-identity-providers). #### Option B: Token exchange Your application obtains an external access token through its own login flow. Medplum validates it with the configured IDP and issues a user-scoped Medplum token. - **Advantages:** - Adds Medplum access to an existing login without a second interactive sign-in. - Your application retains control of the external login and can use external credentials with other services that accept them. - **Tradeoffs:** - Requires an external access token that the IDP's UserInfo endpoint can validate. - Your application manages both external and Medplum credential lifecycles. - **Best fit:** Your application already obtains an external access token and needs to add Medplum access, potentially alongside other services. **Other integrations alone do not require token exchange.** Integrations running in Bots or your backend with service credentials do not require the client to hold an external token. **Select the intended membership.** A user can have several memberships in the client's project. Pass `membership_id` when needed; otherwise token exchange selects the first matching membership. Bind token storage and caches to the user, selected membership, and application session so a tenant switch cannot reuse another membership's token. Cross-project exchange has separate [server-level configuration requirements](/docs/auth/token-exchange#configure-a-server-level-external-auth-provider). ##### If you choose token exchange: where will you keep the Medplum token? Both options below use **user-scoped Medplum tokens**. Choose based on whether the client should call Medplum directly or through your backend. ###### B1: In the client The client retains the exchanged Medplum token and calls Medplum directly. - **Advantages:** - Direct SDK, authenticated React component, and WebSocket subscription access. - No application proxy for Medplum requests. - **Tradeoffs:** - The client manages both credentials, including expiry and logout, with storage appropriate to its platform. - **Best fit:** Direct Medplum access is valuable and credentials can reside in the client. ###### B2: On the server Your backend retains each user's exchanged Medplum token and uses it to fulfill their requests. - **Advantages:** - Medplum credentials stay on the server. - Fits an existing backend that coordinates multiple services, while preserving each user's Medplum access policy. - **Tradeoffs:** - Your backend manages per-user tokens, renewal, and logout. - Proxying adds latency and infrastructure to operate. - Live updates need a backend-to-client delivery path; components expecting direct Medplum access need adaptation. - **Best fit:** Your architecture requires backend mediation or keeping Medplum credentials out of the client. Implementation: [Token Exchange](/docs/auth/token-exchange). #### Option C: On-Behalf-Of (single machine-to-machine user) Your backend uses a single machine-to-machine application identity (`ClientApplication`) and makes requests on behalf of individual human users. Medplum applies the specified user's access policy and records their attribution. - **Advantages:** - Supports a backend that owns authentication without requiring external user tokens that Medplum can exchange. - Preserves user-level access policies and attribution with one application credential. - **Tradeoffs:** - The backend's `ClientApplication` requires Project Admin authority. Treat its credential as privileged. - Your backend must authorize each requested user context and reject missing or unauthorized contexts before calling Medplum; a request without the On-Behalf-Of header uses the application's own permissions. - Proxying adds latency and infrastructure to operate. - Medplum WebSocket subscriptions do not support this On-Behalf-Of pattern. - **Best fit:** A backend must mediate Medplum access and cannot use supported user-token exchange. With server-held token exchange, the Medplum credential represents the user. With On-Behalf-Of, it represents the application, which selects the user context for each request. Prefer user-scoped exchange when either approach meets your requirements. Implementation: [On-Behalf-Of](/docs/auth/on-behalf-of). **Next:** For authorization-code flow or token exchange, choose your matching key below. For On-Behalf-Of, define your backend's user mapping and continue to [Before You Implement](#before-you-implement). ### Decision 3: How will you match external identities to Medplum users? **For external IDP authorization-code flow or token exchange.** Choose which identifier connects the external login to the correct Medplum account. #### Option A: Email matching Medplum matches users by email by default, including with external IDPs. - **Advantages:** - Simple when each person has a distinct email and changes are infrequent. - **Tradeoffs:** - Email changes require updating the Medplum login account, not just the clinical profile's contact email. - Shared email addresses cannot distinguish separate people. - **Best fit:** Distinct emails and manageable account updates. An external IDP alone is not a reason to change the matching key. #### Option B: External ID matching Use the IDP's subject identifier. Enable subject-based matching and include the corresponding `externalId` when provisioning through the [Invite API](/docs/api/project-admin/invite). For token exchange with `useSubject`, provision the complete `sub` value, including any provider prefix. Do not strip an Auth0 prefix or otherwise transform it independently of the login flow. - **Advantages:** - Identity remains stable when contact email changes. - Separate people can have distinct identities even if they share a contact email. - **Tradeoffs:** - Provisioning and login must agree on the identifier; changing IDPs requires a migration plan. - **Best fit:** An established IDP provides stable identifiers, email changes frequently, or people share contact addresses. Pair external IDs with [SCIM](/docs/api/scim/overview) when your IDP should drive user provisioning and lifecycle management in Medplum. Implementation: [Using External IDs](/docs/user-management/external-ids), [Updating User Emails](/docs/user-management/project-vs-server-scoped-users#changing-the-name-and-email-of-a-project-scoped-user). ### Before You Implement #### How will users join your application? A `Patient` or `Practitioner` record alone does not create a login. `User` and `ProjectMembership` are protected resources. For administrator-led provisioning, use the [Invite API](/docs/api/project-admin/invite) to create or link the user, profile, and membership. Other supported entry points include [SCIM](/docs/api/scim/overview) and [patient self-registration](/docs/user-management/open-patient-registration). Choose [project-scoped or server-scoped users](/docs/user-management/project-vs-server-scoped-users) based on whether identities belong to one project or need access across projects. For patient and caregiver applications, give each person their own identity and explicitly grant access to the appropriate records. #### Who manages MFA, recovery, and logout? Use Medplum's MFA for Medplum-managed password login and the external IDP's MFA for external login. Decide how recovery and offboarding work, including ending both external and Medplum sessions where applicable. Medplum's [MFA settings flows](/docs/auth/how-mfa-works#flow-self-enrolling-from-a-settings-page) require an authenticated human user without SMART resource scopes and do not accept On-Behalf-Of. If the app uses SMART-scoped clinical access, plan a supported user authentication context for account settings. Test both flows separately. Implementation: [MFA](/docs/auth/mfa), [Access Token Management](/docs/auth/session-management), [Logout](/docs/auth/logout). Record your choices for each population. Test sign-in, account changes, recovery, token expiry, logout, and offboarding. If your backend mediates access, also verify that requests use the correct user's context and fail when that context is missing. --- ## Care Coordination Decision Guide Use this guide with your clinical and operations teams to decide what work needs to happen, who owns it, and how to follow it through. Start with the practice's first workflow, then add care plans, case tracking, or decision support as needed. _Companion to the [Care Coordination](/docs/careplans) docs._ Start with the work your team needs to coordinate: 1. **[Define work items and delegation](#work-and-delegation).** Decide what each task represents and which steps each role can perform. 2. **[Assign ownership and handoffs](#ownership-and-handoffs).** Define queues, assignment, workflow stages, deadlines, visibility, and responsibilities across teams. 3. **[Plan care across visits](#care-across-visits)** if you need longitudinal cases, care plans, or shared goals. 4. **[Choose automation and decision support](#automation-and-decision-support)** where protocols or clinical recommendations should create follow-up work. 5. **[Define how progress is measured](#how-will-progress-be-measured).** Agree on completion criteria and the operational or clinical outcomes you will track. Use the [questions below](#before-you-start) to establish your use case, then follow the relevant sections. Add requirements beyond these examples where needed. ### Before you start The approaches below name recommended resources and fields. Local workflow stages, eligibility checks, and automation are application conventions that you implement; storing these resources does not execute those rules automatically. #### Which operational problems are you trying to solve first? - Clinicians spend time on work that other team members could perform - Work gets lost between people, teams, visits, or systems - Staff cannot see who owns the next action or what is overdue - Patient care spans multiple visits without a clear case history or plan - Follow-up, protocols, or clinical recommendations are applied inconsistently - Some combination; identify the first workflow you intend to launch *Why: establishes whether the starting point is delegation, a work queue, longitudinal tracking, or a more structured care program.* #### Who participates, and where does responsibility sit? - Which clinicians, nurses, assistants, coordinators, administrative staff, patients, and caregivers perform work? - Is work organized by individual, role, care team, service line, location, or organization? - Who is accountable for a patient's overall care, and who owns each individual action? - Do outside providers or partner organizations participate directly, or through exchanged messages and documents? *Why: separates ongoing care responsibility from task ownership and identifies the access and handoff boundaries.* #### What creates work, and where is it managed today? - Staff action, completed intake, an incoming message, an admission/discharge event, an order or result, a missed appointment, a scheduled follow-up, or a recommendation? - Are you replacing spreadsheets, inboxes, or another task system, or coordinating work that will remain in those systems? - Which system owns the clinical record, the work item, and its completion status? *Why: establishes entry points, integration needs, and which system can authoritatively change each state.* #### How much continuity and scale do you need? - Does work end with one action or encounter, or continue across months of care? - Can a patient participate in several programs or cases at once? - How many staff share a queue, how much work arrives, and are operations limited to business hours? - What must improve to consider the first release successful? *Why: determines the need for case grouping, concurrent assignment controls, escalation, and reporting.* #### Does a care program require consent or service-time records? - What program consent must be captured, reviewed, renewed, or withdrawn before enrollment or ongoing work? - Which activities require time records, and who performed them for which patient and program? - How will you record actual effort, corrections, overlapping activity, and the reporting period required by the program or payer? *Why: identifies program-specific consent and time-recording requirements. Task execution windows alone do not establish billable effort. Choose the time-record model and any Consent representation after confirming the program's requirements; see [Consent](/docs/consent).* ### Work and delegation #### What does each work item represent? Decide what a work item represents and how it relates to the clinical record. - What is the smallest unit of work that needs its own owner, status, and completion criteria? - Is each item about a patient, a particular encounter, or practice administration? - What is being acted on: a referral, result, questionnaire, message thread, or another resource? - Does one person perform the whole activity, or do independently assigned steps need separate work items? | Situation | Approach | | :---- | :---- | | A follow-up action needs tracking | Use `Task` for the work. Link `Task.focus` to the resource being acted on and `Task.for` to the patient when applicable. | | Work arises from a particular visit | Use `Task.encounter` for encounter context where appropriate; patient-level work can exist without an encounter. | | Several steps have different owners or completion criteria | Use separate Tasks. Use `Task.partOf` when a parent/subtask relationship is useful; start with a shallow hierarchy. | | A clinical order already exists | Keep the order, such as a `ServiceRequest`, and point `Task.focus` to it. Completing a preparation or scheduling Task should not complete the order; update the order only when its own fulfillment criteria are met. | | Non-patient administrative work | Use `Task.code` for the work type and `owner` for the responsible person or organization. Omit `for` when there is no beneficiary; use `focus` only when an actual resource is being acted on. Scope access by organization or role. | See [Tasks and Work Queues](/docs/careplans/tasks). #### Which steps can each role perform? Decide which steps each role can perform and where clinical judgment or review is required. - Which steps currently reach a clinician that another qualified team member could prepare or complete? - Which actions require clinical judgment, sign-off, supervision, or escalation? - Do eligibility rules depend on credentials, specialty, service line, jurisdiction, or the patient's program? - Who maintains those rules, and what happens when no eligible person is available? | Situation | Approach | | :---- | :---- | | Work can be routed by role | Use `Task.performerType` to describe the required role and `Task.owner` for the accountable assignee or group. Role labels alone do not establish eligibility. | | Preparation and clinical review are separate responsibilities | Create separate preparation and review Tasks. Reference the prepared artifact in `Task.output.valueReference`, then supply it through the review Task's `input.valueReference`. Each Task has its own owner and completion criteria. | | Eligibility varies by service or jurisdiction | Use `PractitionerRole.healthcareService` for the service and `PractitionerRole.location` for licensed jurisdictions. Evaluate role, credentials, and current eligibility in the controlled assignment or booking path before setting `Task.owner`. | | No eligible assignee is available | Set `Task.status` to `on-hold`, record the reason in `statusReason`, and assign `owner` to an escalation team or coordinator. Use a local `businessStatus` to make these items searchable as an exception queue. | Define the role/action matrix with the practice's clinical and operational owners. Enforce data access using [Access Policies](/docs/access/access-policies). For booking, see [State-by-State Licensure](/docs/scheduling/state-by-state-licensure); scheduling operations do not enforce licensure themselves. ### Ownership and handoffs #### How will staff find, claim, and reassign work? Decide how staff discover work, take responsibility, and hand it to someone else. - Is work assigned directly, claimed from a shared queue, or distributed automatically? - Which queue dimensions matter: role, service, location, urgency, or workflow stage? - Can two staff members attempt to claim the same item? Who resolves conflicts? - Does the patient have an ongoing care team or coordinator distinct from the person handling today's task? - How do absence, workload, and shift changes affect assignment? - How do staff discover new or escalated work: a live queue, an outbound notification, or both? See [assignee notifications](/docs/careplans/automating-workflows#let-the-assignee-know). | Situation | Approach | | :---- | :---- | | Direct individual assignment | Set `Task.owner` to the responsible person or practitioner role. | | Shared queue | Build a filtered view of Tasks using work type (`code`), lifecycle (`status`), local stage (`businessStatus`), and required role (`performerType`). Use a group `owner` for team accountability, or leave it unset for an unclaimed role pool. | | Staff claim work concurrently | Update `Task.owner` using `If-Match` with the version read. On `412 Precondition Failed`, reload and re-evaluate the claim. Do not retry by overwriting the new owner. | | Ongoing patient care team | Set `CareTeam.subject` to the patient; record members, roles, and participation periods in `participant`. Link the team through `CarePlan.careTeam` or `EpisodeOfCare.team` as appropriate. Tasks retain their own owners. | | Reassignment or cross-coverage | Update `Task.owner` for the same work item and retain resource history. Add `Provenance` targeting the Task when a structured actor and transfer reason are needed. A separately accountable handoff step gets its own Task. | Care team membership and task assignment describe responsibility; neither automatically grants access to related patient data. See [Message Response Tracking and Routing](/docs/communications/message-response-tracking-and-routing), [Version Checking](/docs/fhir-datastore/updating-data#preventing-lost-updates-with-version-checking), and [Resource History](/docs/fhir-datastore/resource-history). #### What moves work forward, and when is it overdue? Decide what each stage means and what evidence allows work to move forward. - Which stages do staff need to operate on, and which are useful only for reporting? - What counts as ready, blocked, complete, cancelled, or entered in error? - Does handing off work require acknowledgment from the next owner? - Which deadlines use elapsed time versus business hours? Does waiting for a patient or outside party pause the clock? - Who receives overdue work, and how are repeated reminders prevented? | Situation | Approach | | :---- | :---- | | Standard lifecycle plus local workflow stages | Use `Task.status` for lifecycle, `businessStatus` for local stages, and `statusReason` for why work is blocked or cancelled. | | Completion requires a resulting artifact | Use `Task.output.valueReference` for the result, such as a `QuestionnaireResponse`, `DocumentReference`, or response `Communication`. Validate the required output before setting `status` to `completed`; record actual completion in `executionPeriod.end`. | | Several independently tracked handoffs | On completion of the prerequisite Task, have the application or Bot conditionally create the next Task with its owner and required inputs. Use `partOf` for hierarchy; implement transitions through the [handoff workflow](/docs/careplans/handoffs-and-escalation#release-the-next-step). | | Due dates or response targets | Use `restriction.period.end` for the deadline and `executionPeriod` for actual work. Follow the [planned-date convention](/docs/careplans/tasks#task-start--due-dates) for general queues and exchange. Calculate business-hour deadlines and paused intervals in application or Bot logic. | | Time in each stage matters | Derive stage changes from Task resource history; add `Provenance` when actor, reason, or effective transition time needs explicit recording. Build reporting intervals from those changes. `Task.lastModified` alone is insufficient. | See the [Task guide](/docs/careplans/tasks) for status and timing fields. Queue ordering, deadline calculations, and escalation require application or Bot logic. #### What can participants see, and how will external handoffs work? Decide what each participant can see and how work crosses organizational boundaries. - Do patients or caregivers perform assigned actions, see progress, or only receive updates? - Which notes and documents are internal, patient-visible, or intended for an external recipient? - After reassignment or discharge, who retains access to the record? - Do external partners update shared records, exchange structured data, or return documents and messages? - How are replies matched to the original request, and who handles unmatched responses? | Situation | Approach | | :---- | :---- | | Patients or caregivers complete assigned work | Set `owner` to Patient or RelatedPerson and `for` to the patient. For forms, focus on the Questionnaire and return the QuestionnaireResponse in Task output; give staff review a separate Task. See [patient work lists](/docs/careplans/tasks#give-patients-and-caregivers-their-own-work-list). | | Participants need different visibility | Enforce access through `AccessPolicy` and `ProjectMembership`; define permissions for each relevant resource type. | | Patient-related resources are linked together | Populate each resource's own patient-compartment references. A link to a Task, CarePlan, or message thread does not by itself propagate patient access. | | Work includes messaging | Point `Task.focus` to the `Communication` thread header and `Task.for` to the patient. Reference the resolving message in `Task.output.valueReference`. Complete the Task only when the response satisfies the workflow's completion criteria. | | Documents support a handoff | Store file content in `Binary` and describe it with `DocumentReference`. Use an attachment URL such as `Binary/{id}` and set the Binary's `securityContext` appropriately; do not embed base64 in `Attachment.data`. | | A referral transfers a request for services | Use the [Referrals Decision Guide](/docs/decision-guides/referrals) for capture, transmission, receiving, and closure. Connect its handoffs to the shared ownership and timing rules agreed here. | Use the [Access Control Decision Guide](/docs/decision-guides/access-control), [Messaging Decision Guide](/docs/decision-guides/messaging), and [Binary Data](/docs/fhir-datastore/binary-data) docs for the detailed designs. Scheduling and billing decisions should likewise link to their respective workflow documentation. ### Care across visits #### Does care need a longitudinal case identity? Decide whether care needs an identity and lifecycle that spans multiple visits. - What defines a case: a condition, a treatment course, a service program, or a period of responsibility? - What starts, pauses, transfers, and ends that case? - Can a patient have concurrent cases, including more than one addressing the same condition? - Can one encounter contribute to multiple cases? - Which organization, coordinator, or care team is responsible over time? | Situation | Approach | | :---- | :---- | | Independent tasks or a single visit are sufficient | Keep patient and encounter context; add case grouping when a distinct longitudinal lifecycle is needed. | | Care spans multiple visits under a defined responsibility | Use `EpisodeOfCare` with `patient`, `status`, `period`, and `managingOrganization`. Use `careManager` and `team` for ongoing responsibility, and link visits through `Encounter.episodeOfCare`. | | Concurrent programs or treatment courses | Give each episode its own identity and lifecycle; an Encounter can reference more than one episode. | | A case also needs a clinical plan | Follow the longitudinal guide's convention: reference the `EpisodeOfCare` in `CarePlan.supportingInfo`. This is an explicit supporting-context link, not a dedicated FHIR case-membership field; agree whether a plan may span multiple episodes. | | Several cases share a diagnosis | Keep diagnoses in `EpisodeOfCare.diagnosis.condition` and `CarePlan.addresses`. When finding a case's plans, verify their explicit `supportingInfo` references; a patient or condition search alone can return plans for other cases. | See [Longitudinal Patient Case Tracking](/docs/careplans/longitudinal-patient-case-tracking). #### How will care plans, goals, and progress be managed? Decide whether the team needs a coordinated clinical plan in addition to its work queue. - Do staff need to see agreed goals and planned activities together for a patient? - Who authors, approves, reviews, and changes the plan? Does the patient participate? - How is goal progress assessed, and which observations or assessments support it? - When a plan changes, what happens to already assigned work? - What closes the plan, and can individual goals or activities finish at different times? | Situation | Approach | | :---- | :---- | | A patient-specific plan is needed | Set `CarePlan.subject` to the patient, `addresses` to the relevant Conditions, `goal` to Goals, and `activity.reference` to planned requests or Tasks. Use `careTeam` for the people responsible for the plan. | | Goals need independent tracking | Reference each `Goal` through `CarePlan.goal`. Use `Goal.target` for the measure, target value, and due date; `achievementStatus` for progress; and `outcomeReference` for supporting Observations. Keep goal lifecycle separate from achievement. | | Planned activities create staff work | For directly authored work, reference the Task in `CarePlan.activity.reference` and the authorizing plan in `Task.basedOn`; keep `Task.focus` for the item being acted on. Assess goal achievement separately from Task completion. | | Plans change during treatment | Update the existing CarePlan for routine revisions. For a replacement plan, use `CarePlan.replaces` and close the prior plan appropriately. Explicitly retain, cancel, or replace outstanding Tasks; changing a plan does not migrate them automatically. | See [Care Plans and Goals](/docs/careplans/care-plans-and-goals) and [Clinical Protocols](/docs/careplans/protocols) for the distinction between a patient's plan and a reusable protocol. ### Automation and decision support #### Which work should protocols and automation create? Decide which patterns should repeat and which decisions remain with people. - Is work created manually, from events, on a schedule, or from a reusable protocol? - Which steps repeat for every patient, and which depend on prior outcomes? - Are there dependencies, delays, repeated attempts, or stop conditions? - When a protocol changes, do existing patients continue their current version or migrate? - How are failed runs, duplicate events, and missed follow-ups detected and recovered? | Situation | Approach | | :---- | :---- | | Staff initiate occasional work | Create a Task with `code`, `status`, `intent`, patient context where applicable, and an owner or required role. Add `focus` for the resource being acted on. | | A reusable clinical protocol is useful | Use `PlanDefinition` and `ActivityDefinition`. Medplum's [`$apply`](/docs/api/fhir/operations/plandefinition-apply) creates a CarePlan whose activity references a RequestGroup with Task actions. Check generated resources and supported behavior against the workflow before adopting it. | | A resource event should create or advance work | Use tightly scoped Subscriptions and Bots. Select create/update events and transition criteria where needed to avoid triggering on unrelated edits. | | Recurring or overdue work | Use a scheduled Bot to create one Task per due occurrence. Put a stable source/step/occurrence key in `Task.identifier`; stop creating work when the source plan or case is no longer eligible. For overdue work, update or escalate the existing Task. | | Events may be processed more than once | Conditionally create generated Tasks by `identifier`, for example with `createResourceIfNoneExist`. Reprocessing the same event must reuse the same key. Track and retry external delivery separately; avoiding duplicate Tasks does not make an external send idempotent. | | Conditional or dependent workflow steps | Use scoped transition Subscriptions and Bots to evaluate prerequisites and create or release the next Task. Implement the dependency rules in the application or Bot. | For event-driven workflows, check subscription scale, tenant scope, execution identity, and failure recovery in [Planning Automations](/docs/subscriptions#planning-automations). See also [Clinical Protocols](/docs/careplans/protocols), [Subscription Extensions](/docs/subscriptions/subscription-extensions), [Cron Jobs for Bots](/docs/bots/bot-cron-job), and [Working with FHIR Data](/docs/fhir-datastore/working-with-fhir). #### When should clinical decision support create follow-up work? Decide how clinical guidance enters the workflow and whether it creates an obligation to act. - Do clinicians need contextual reference material, rules-based recommendations, or predictive outputs? - When should guidance appear: chart review, order entry, assessment review, or background monitoring? - Is the output informational, does it require acknowledgment, or should accepted recommendations create work? - Who can accept, dismiss, or override a recommendation, and what reason or evidence should be recorded? - Who maintains the knowledge or model, and how will you evaluate usefulness, missed follow-up, and excessive alerts? | Situation | Approach | | :---- | :---- | | Guidance is needed during a clinical interaction | Configure a Bot's `cdsService` for the hook and prefetch requirements; have the client invoke it at the chosen workflow point and display returned cards. A card does not itself create a persistent work item. | | A recommendation needs review and follow-up | Use a review Task with patient context and the relevant evidence in `focus` or `input`. Record the disposition in locally defined `businessStatus` values and the rationale in `note`. Reference any resulting clinical request in `output`; track its fulfillment with a separate Task. | | Background rules identify patients for review | Have a Bot conditionally create a review Task with `for`, `owner` or `performerType`, and an `identifier` derived from the rule and triggering event or review period. Repeated evaluations should reuse that work item until a new review is warranted. | | An external CDS Hooks service is used | Map the supported hook's context and prefetch to the service request, then render its cards in the client. Implement timeout and failure handling there; create a fallback review Task only when the workflow requires human follow-up. | See [Clinical Decision Support](/docs/careplans/clinical-decision-support) for broader CDS considerations and [CDS Hooks](/docs/integration/cds-hooks) for the integration contract. Define clinical review and applicable governance requirements alongside the workflow design. If every evaluation must be retained, including informational results with no follow-up, choose that persistence model separately. A review Task records work and its disposition; it is not a complete CDS evaluation record. See the [GuidanceResponse design choice](/docs/careplans/clinical-decision-support#decide-whether-to-retain-every-evaluation). ### How will progress be measured? Decide what the practice needs to measure and what each completion signal means. - Which daily views do staff need: unassigned work, due work, blocked items, workload by team, or patients needing follow-up? - Which measures demonstrate improvement: turnaround time, fewer handoffs, appropriate delegation, successful follow-up, or goal progress? - What timestamps and event history support those measures, including reopened work and paused clocks? - Does completing a Task also complete an activity, referral, plan, or case? Under what conditions? | Situation | Approach | | :---- | :---- | | Staff need actionable work lists | Query Tasks by `owner`, `status`, `code`, and `business-status`, within the caller's access policy. Use `owner:missing=true` for unowned pools; group-owned queues require the group's owner reference instead. | | Managers need aggregate or historical metrics | Build a reporting table with Task ID, previous/new stage or owner, transition time, actor, and reason, derived from history and any explicit Provenance. Calculate stage durations and paused intervals from this event sequence. Store the derived intervals in the reporting system. | | Care outcomes matter | Measure goal achievement through `Goal.achievementStatus`, targets, and the Observations in `outcomeReference`. Report those alongside completed Tasks; Task completion alone does not demonstrate an improved clinical outcome. | | Work, plans, and cases close independently | Update each applicable lifecycle explicitly: `Task.status`, `CarePlan.status`, `Goal.lifecycleStatus`, and `EpisodeOfCare.status`. Before closing a case, resolve or reassign its open work; no automatic cascade should be assumed. | ### Decisions to Record Before Implementation - The first workflow, its entry event, and its source of truth - Work items, clinical context, and any longitudinal case or care-plan model - Eligible roles, accountable owners, review steps, and exception handling - Lifecycle stages, completion evidence, deadlines, and escalation rules - Automation and CDS behavior, including human review and failure recovery - Access boundaries, external handoffs, and the measures of success --- ## Charting Decision Guide Use this guide to design how clinicians document care in your application. Work through encounter types, Visit Templates, structured data, signing, and template ownership to decide what the chart captures and how that information becomes usable FHIR resources. _Companion to the [Charting](/docs/charting) docs (SOAP notes, diagnoses, vitals, allergies), plus [Provider Visits](/docs/provider/visits), [Structured Data Capture](/docs/questionnaires/parsing-questionnaire-responses), [`PlanDefinition $apply`](/docs/api/fhir/operations/plandefinition-apply), and [`QuestionnaireResponse $extract`](/docs/api/fhir/operations/extract)._ For each clinical workflow, make five decisions: 1. **[Define visit types and their contents](#what-visit-types-do-you-provide-and-what-does-each-entail).** Decide what each encounter requires and how Visit Templates organize it. 2. **[Choose how structured the chart should be](#how-structured-should-the-chart-be).** Balance narrative documentation with fields needed for clinical workflows, reporting, and reuse. 3. **[Choose how form responses become queryable data](#how-will-form-responses-be-parsed-into-queryable-data).** Define extraction and the resources it creates or updates. 4. **[Define signing requirements](#what-are-the-signing-requirements).** Decide what is signed, who can sign or co-sign, and how later changes are handled. 5. **[Assign Visit Template ownership](#who-owns-and-edits-visit-templates).** Decide who can edit templates and how changes are governed. Use the [clinical context below](#before-you-start) to guide these choices. Treat the options as starting points, assign an owner to unresolved questions, and follow the linked technical docs for implementation. ### Before you start #### What care setting and specialty? - Primary care - Urgent care - Specialty (cardiology, dermatology, orthopedics, etc.) - Behavioral health - Multi-specialty / mixed *Why it matters: care settings have different visit templates and charting needs. Behavioral health leans on standardized screening instruments; primary care emphasizes vitals and preventive care; specialty care often runs on protocol-driven order sets. Shapes every Visit Template in [Visit types](#what-visit-types-do-you-provide-and-what-does-each-entail).* #### What types of encounters will be charted? - In-person visits - Telehealth visits - Async / messaging-based encounters - Group visits - Some combination *Why it matters: encounter types may need distinct Visit Templates. Async encounters chart differently — the Subjective section may come from a messaging thread. Group visits raise the question of one note per patient vs. a shared note.* #### What are clinicians used to today, and what do they want to keep consistent? - What EHR or charting tool are clinicians coming from? - What note formats or workflows do they want to preserve? - What gaps drove the move? *Why it matters: charting changes have high adoption risk. Knowing what clinicians expect to stay the same is as important as knowing what to improve.* ### What visit types do you provide, and what does each entail? The recommended baseline is one Visit Template (PlanDefinition) per distinct visit type, combining structured forms (Questionnaires) with embedded order actions (ActivityDefinitions). `$apply` generates the CarePlan and Tasks at visit start. **⚠ Visit types are the bridge to scheduling.** Define the mapping between charting Visit Templates and bookable HealthcareService offerings. Scheduling operations use explicit HealthcareService references, not code matching alone. See the [Scheduling Decision Guide](/docs/decision-guides/scheduling). **Questions:** - What are the distinct visit types? (annual wellness, acute sick, behavioral health intake, post-op follow-up, specialty consult, etc.) - For each visit type, what does the clinician do during the encounter? - Measurements (vitals — BP, HR, weight, BMI, temp, SpO2) - Screening instruments (PHQ-9, GAD-7, AUDIT, fall risk, pediatric screens) - Orders (labs, imaging, referrals, prescriptions) - Narrative documentation - Review of patient history (problem list, allergies, meds) **What the answers drive:** For each visit type, the elements map to PlanDefinition components: | Visit element | How it's modeled in the PlanDefinition | | :---- | :---- | | Vitals | Questionnaire action; parses responses to Observations (`category = vital-signs`, LOINC codes) | | Screening instruments (PHQ-9, GAD-7, etc.) | Questionnaires per each standard form | | Labs, imaging, and referrals | ActivityDefinition with `kind: ServiceRequest`; `$apply` creates the ServiceRequest and associated Task | | Prescriptions | Create MedicationRequest through the prescribing workflow; `$apply` does not generate MedicationRequest resources | | Narrative documentation | `ClinicalImpression.note` (captures the 'Assessment' part of SOAP) | | Patient history review | Surfaced via the patient sidebar (see [Chart structure](#how-structured-should-the-chart-be)), not modeled in the PlanDefinition itself | If visit types share most of their structure, factor common pieces into shared Questionnaires / ActivityDefinitions referenced by multiple PlanDefinitions. **Execution boundary:** `$apply` processes top-level actions; it does not execute nested actions or evaluate `action.condition` as a guard. Use application or Bot logic to select and release conditional steps. Supported ServiceRequest `ActivityDefinition.dynamicValue` mappings can populate generated fields, but do not provide conditional workflow execution. Verify the generated resources before using a template for clinical work. ### How structured should the chart be? **Medplum Recommendation:** Structured wherever possible. Medplum's documented [SOAP note approach](/docs/charting/visit-templates) maps each section to discrete resources — Observations for findings, ClinicalImpression for assessment, ServiceRequest / MedicationRequest / CarePlan for the plan. Reserve free-text (`ClinicalImpression.note`) for the assessment (A) component of SOAP. **⚠ Recommended pattern.** Surface a patient sidebar (problem list, allergies, active medications) alongside the active note, with inline edit. **Questions:** - For each visit type from [Visit types](#what-visit-types-do-you-provide-and-what-does-each-entail), what mix of structured forms vs. free-text does the workflow require? - Are orders placed inline within the chart, or in a separate orders area? **What the answers drive:** | Decision | Medplum Recommendation | Alternative (and tradeoff) | | :---- | :---- | :---- | | Structured vs. freeform | Structured forms; free-text only where clinicians require it | Heavy free-text. Tradeoff: not queryable; does not leverage FHIR data advantages | | Order placement | Inline in the Visit Template, generated as Tasks | Separate orders module. Tradeoff: loses inline ordering context | ### How will form responses be parsed into queryable data? **Medplum Recommendation:** always parse — without parsing, form data isn't queryable for analytics, alerting, or dashboards. The decision is *how*, driven by parsing complexity. **Questions:** - Does parsing need complex conditional behavior (score thresholds, external calls, multi-resource creation)? **What the answers drive:** | Situation | Approach | | :---- | :---- | | Logic is relatively simple | **SDC \+ `$extract`.** Annotate the Questionnaire with SDC extensions; invoke `$extract` on submission. Less flexible, but extraction logic lives in one place with the form | | Logic is complex | **Subscription \+ Bot.** Subscribe to QuestionnaireResponse and parse in code. More flexible (conditional logic, external calls), but Bot code changes are required when the form changes | ### What are the signing requirements? The standard sign-off pattern marks `ClinicalImpression.status = completed` and creates Provenance targeting the Encounter to record the signer and timestamp. Define which content and resource versions the sign-off covers; an Encounter reference alone does not freeze the visit chart. Signing, locking, co-signing, and amendments need explicit application rules. **Questions:** - Which resource versions or document snapshot make up the signed content? - Is co-signing required (attending co-signs resident, provider co-signs scribe)? - Should signing lock immediately, or leave a post-sign editable window? - Will amendments be possible / needed after a chart is locked? - Is a queryable audit trail of every signing event required? **What the answers drive:** | Situation | Approach | | :---- | :---- | | Single signer | ClinicalImpression `completed` plus Provenance recording attribution; identify the signed resource versions or retained document snapshot | | Co-signature | Append additional Provenance entries per co-signer; surfaces multi-signer state. Typically a custom build | | Lock timing | Choose immediate locking or an editable window; enforce the chosen write restrictions at the API. Creating Provenance does not lock resources | | Post-lock amendments | Retain the original signed content and record an addendum, for example a DocumentReference linked to the Encounter; define how it references and supplements the original | | Audit trail | Provenance per signing event; query via `Provenance?target=Encounter/{id}` | ### Who owns and edits Visit Templates? **Default:** engineering implements template changes from a spec maintained by clinical operations — clinical ops owns *what* should change, engineering implements via API or migration scripts. Works well when changes are infrequent or warrant review. If clinical ops needs to edit directly, the Medplum App provides a builder interface out of the box. It's relatively bare-bones (FHIR-shaped), so customers wanting a more visual or user-friendly experience for non-technical users can build a custom admin UI on top. **Questions:** - Will engineering implement template changes from a clinical-ops spec, or does clinical ops need to edit directly? **What the answers drive:** | Situation | Approach | | :---- | :---- | | Engineering implements from a spec (default) | API or migration scripts; treated as code-reviewed artifacts | | Clinical ops edits directly | Use the Medplum App's builder interface out of the box; build a custom admin UI on top if a more visual / user-friendly experience is required | --- ## Data Migration Decision Guide Use this guide to plan a migration to Medplum and decide what moves, which system owns each domain, and how the transition will be validated. Work through identity mapping, import operations, cutover, and exception handling to define an approach with clear acceptance criteria and owners. _Companion to the [Migrating to Medplum](/docs/migration) docs._ Plan the migration around four decisions: 1. **[Define scope, identity, and mapping](#scope-identity-and-mapping).** Agree on the data and history to move, identifier and deduplication rules, FHIR mappings, and acceptance owners. 2. **[Choose loading and validation](#loading-and-validation).** Select an import approach that meets throughput and recovery needs, and define evidence that the migrated data is correct. 3. **[Plan cutover and rehearsal](#cutover-and-rehearsal).** Decide when write authority changes, how later updates are handled, and what rehearsals must demonstrate. 4. **[Plan protection and operational handoff](#protection-and-operational-handoff).** Define access and audit controls, preserve usable documents and binaries, and assign monitoring and exception ownership. Use the [context questions](#before-you-start) to establish the migration's purpose and owners, then check the [factors that change the plan](#factors-that-change-the-migration-plan) for additional design and validation needs. ### Before you start #### What outcome defines success? - Replace a legacy clinical or operational system - Consolidate several systems into one Medplum project - Backfill history while the source remains active - Seed selected data for a new workflow - Preserve a read-only archive - Build a repeatable onboarding pipeline for future sources _Why: determines whether this is a finite conversion, ongoing synchronization, or a reusable product capability._ #### Which sources and interfaces are in scope? For each source, identify: - Business and technical owners - Extraction method and available formats - Data volume and rate of change - Stable identifiers - Time zone, locale, and encoding conventions - Known quality issues and source variants - Deletion, merge, and correction history - Whether the source remains active after cutover _Why: a database snapshot, FHIR endpoint, HL7 feed, document archive, and DICOM repository need different extraction and reconciliation plans._ #### What data and history are required at go-live? Consider: - People, organizations, locations, and coverage - Encounters, appointments, current clinical state, and clinical history - Forms, communications, tasks, consents, and other workflow state - Documents, files, and imaging - Billing, terminology, profiles, and configuration For each domain, choose full history, current records only, a date range, summary data with legacy archive access, or metadata with files retained elsewhere. _Why: "all data" is not an actionable scope. Each domain needs an explicit boundary, owner, target, and acceptance criteria._ #### Where is each domain authoritative during and after migration? - Source remains authoritative - Medplum becomes authoritative at cutover - Authority changes in phases - An external system remains authoritative for a domain - Both systems coexist under an explicit conflict policy _Why: coexistence without one write authority per domain creates conflicts and makes reconciliation inconclusive._ #### Who approves the migration? Assign accountable owners for: - Source meaning and extraction - Patient identity - FHIR mapping and profiles - Terminology and clinical interpretation - Security and compliance - Pipeline operations and reconciliation - Go-live and rollback _Why: many migration decisions require clinical, operational, and compliance judgment rather than engineering judgment._ #### What operational change accompanies the migration? - Which user workflows, reports, and integrations change at cutover? - How much planned downtime can each workflow tolerate? - Which users need training, and who owns readiness? - Who communicates the freeze, cutover, rollback, and return-to-service status? - What support coverage and post-go-live support period are required? - How are user-reported data discrepancies triaged after cutover? _Why: technically correct data is not a successful migration if users cannot safely perform their work after go-live._ ### Factors that change the migration plan Every migration must define scope, identifiers, mapping ownership, validation, security, testing, cutover authority, and acceptance. The matrix below identifies additional complexity, not optional safeguards. Use these factors to identify where the migration needs additional design and validation. | Factor | What it changes | Guidance | | :--- | :--- | :--- | | Multiple source systems, versions, or local variations | Inventory, identifier systems, and mapping layers | [Scope and acceptance](#what-data-and-history-will-move-and-who-accepts-them), [FHIR mapping](#how-will-fhir-mappings-and-terminology-be-governed) | | Cross-source or existing-record patient matching | Identity workflow and reference sequencing | [Identity and deduplication](#how-will-identifiers-matching-and-deduplication-work) | | Automated merge execution during migration | Review thresholds, audit, and reversal risk | [Identity and deduplication](#how-will-identifiers-matching-and-deduplication-work) | | Custom profiles, extensions, or implementation-guide conformance | Mapping approval and validation configuration | [FHIR mapping](#how-will-fhir-mappings-and-terminology-be-governed), [Validation](#what-evidence-establishes-correctness-and-acceptance) | | One source record producing several linked FHIR resources | Stable IDs, all-or-nothing writes, and reconciliation | [FHIR mapping](#how-will-fhir-mappings-and-terminology-be-governed), [Load architecture](#which-load-architecture-meets-throughput-and-recovery-needs) | | Expected load time may exceed the cutover window or service limits | Import path, throughput testing, and recovery | [Load architecture](#which-load-architecture-meets-throughput-and-recovery-needs), [Dress rehearsals](#what-must-testing-and-dress-rehearsals-demonstrate) | | The source remains active while data moves | Incremental changes, write authority, and conflicts | [Write authority](#when-does-write-authority-change-and-how-are-later-changes-handled) | | Little or no downtime is acceptable | Change capture, rehearsal depth, and rollback | [Write authority](#when-does-write-authority-change-and-how-are-later-changes-handled), [Dress rehearsals](#what-must-testing-and-dress-rehearsals-demonstrate) | | Documents, files, images, or DICOM | Storage, access, integrity, and retrieval testing | [Files and complex data](#how-will-documents-binaries-and-complex-data-remain-usable) | | Historical workflows must remain operational | Domain-specific modeling and validation | [FHIR mapping](#how-will-fhir-mappings-and-terminology-be-governed), [Validation](#what-evidence-establishes-correctness-and-acceptance) | | External systems remaining authoritative after cutover | Long-term ownership and synchronization | [Write authority](#when-does-write-authority-change-and-how-are-later-changes-handled), [Operational ownership](#who-monitors-runs-resolves-exceptions-and-accepts-operational-ownership) | | Reusable onboarding or ongoing synchronization pipeline | Operational ownership, versioning, and reconciliation | [Scope and acceptance](#what-data-and-history-will-move-and-who-accepts-them), [Operational ownership](#who-monitors-runs-resolves-exceptions-and-accepts-operational-ownership) | Cover every topic, adjusting the depth to your migration. Include documents, binaries, and complex data only when those domains are in scope. For each topic, record the decision, accountable owner, and acceptance criteria. ### Scope, identity, and mapping #### What data and history will move, and who accepts them? Decide what migrates, how far back each domain goes, and who can accept the result. See [Planning your Migration](/docs/migration/migration-planning). **Recommendation:** Profile the qualifying source population before committing dates, volumes, or validation samples. Give every domain explicit inclusion rules, exclusions, and an acceptance owner. **Questions:** - Which domains and history are required on day one, and what is excluded? - Which sources contribute to each domain, and can they produce a consistent snapshot or a reliable list of later changes? - What are the qualifying record counts, file volumes, and change rates? - Who owns source meaning, mapping, acceptance, and retained legacy access? | Situation | Approach | | :------------------------------------- | :--------------------------------------------------------------------------------------------------- | | Small, inactive source | Use a finite extract, transform, load, and reconcile process with a defined freeze window. | | Active source | Take a baseline snapshot, then process later changes from a recorded source position until cutover. | | Several sources | Keep separate inventories, identifier systems, mappings, and reconciliation results for each source. | | Only current state is required | Define "current" separately for each domain; do not apply one generic filter to all resources. | | Full history stays in a legacy archive | Define what moves, how older data is retrieved, how long the archive remains, and who owns access. | | Scope is uncertain | Profile representative source data before committing dates or sample sizes. | **Output:** Complete the [Source and Domain Inventory](/docs/migration/migration-planning#source-and-domain-inventory). #### How will identifiers, matching, and deduplication work? Decide whether a source record remains distinct, maps to an existing resource, or enters a master-record workflow. See [Patient Deduplication Architectures](/docs/fhir-datastore/patient-deduplication) and [Patient `$match`](/docs/api/fhir/operations/patient-match). **⚠ One-way door:** Loading dependent clinical data before resolving identity can spread references across duplicate patients. Later merges may require extensive repair and clinical review. **Recommendation:** Preserve source identifiers using a different identifier system URI for each source. Keep ambiguous matches separate and require human review unless an approved matching policy permits automatic action. **Questions:** - Which source keys are stable and unique, and which identifier system URI represents each source? - Where can duplicates occur: within a source, across sources, or against Medplum? - What evidence and risk threshold permit automatic matching versus human review? - Will migration merge records, or only report candidates for later remediation? | Situation | Approach | | :------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ | | Stable source key | Preserve it in `identifier` under a source-specific system URI; use it for writes that are safe to repeat. | | Different systems reuse the same value | Use distinct identifier system URIs; never treat the bare value as globally unique. | | Source patient may match an existing Patient | Run the approved identity workflow before loading dependent records. | | Match is ambiguous | Keep records separate and route to human review. | | Existing target ID is not known | Use a conditional reference only when the identifier finds exactly one resource and the migration identity can search it. | | Related resources are created together | Use a transaction with `urn:uuid` references. | | Related resource was loaded earlier | Use a source-to-target ID lookup or a conditional reference that resolves uniquely. | | Keys are missing or unstable | Define a governed replacement key before loading; do not use mutable demographics as the key for repeated runs. | A source identifier answers "which source record is this?" It does not by itself answer "which real-world person is this?" #### How will FHIR mappings and terminology be governed? Decide how source meaning maps to FHIR R4 and how decisions are versioned and approved. See [Governing Data Mappings](/docs/migration/mapping-governance) and [Converting Data to FHIR](/docs/migration/convert-to-fhir). **⚠ One-way door:** Changing identifier systems, profile or extension URLs, resource boundaries, or code systems after applications depend on them can require both data and application migrations. **Recommendation:** Version mappings independently from pipeline code, preserve source meaning when no verified standard mapping exists, and require domain review before production. **Questions:** - Which FHIR resources, fields, profiles, and terminology represent each source concept? - Which mappings are direct, conditional, one-to-many, unsupported, or excluded? - How are missing, ambiguous, free-text, and source-specific values handled? - Are profiles declared per resource in `meta.profile` or applied through `Project.defaultProfile`? - Who approves changes, and which records must be reprocessed when a mapping changes? | Situation | Approach | | :--------------------------------------- | :-------------------------------------------------------------------------------------- | | Direct mapping | Record source meaning, target path, transformation, null behavior, and validation. | | One source entity spans several concepts | Create distinct resources and preserve relationships. | | Trustworthy standard code | Preserve its system, code, and display under the approved mapping. | | Local terminology only | Preserve a governed local code and text; standard-code enrichment is a separate review. | | No verified code mapping | Retain source meaning and route for review; do not invent a code. | | No standard field fits | Check established extensions and profiles before defining a custom extension. | | One profile applies by resource type | Configure `Project.defaultProfile`; it applies when the resource has no `meta.profile`. | | Profile varies by source record | Set the applicable profile explicitly in `meta.profile`. | | Mapping changes after rehearsal | Version the mapping and code together; define reprocessing or cleanup. | Use the [mapping register example](/docs/migration/mapping-governance#mapping-register-example) to define the information your project must track. Mapping approval is distinct from code review. ### Loading and validation #### What evidence establishes correctness and acceptance? Decide how the team will prove structural conformance, referential integrity, semantic correctness, completeness, and workflow readiness. See [Validating and Reconciling Migrated Data](/docs/migration/validation-and-reconciliation) and [Testing and Accepting Migrated Data](/docs/migration/testing-and-acceptance). **Recommendation:** Validate every record structurally and account for every eligible source record. Supplement automated checks with repeatable, random, and risk-based chart review; require named sign-off. **Questions:** - What must reconcile exactly, and which differences are expected and explainable? - Which relationships, values, and high-risk cohorts require deeper review? - How is every success, rejection, skip, retry, record held for review, and exception reported? - Which evidence and thresholds must each stakeholder approve? | Situation | Approach | | :------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | One source row maps to one resource | Reconcile eligible, successful, rejected, skipped, and held-for-review counts. | | One source row maps to several resources | Reconcile each target type and expected relationship separately. | | Deduplication changes counts | Report source records, master records, match decisions, and unresolved candidates separately. | | Profile conformance is required | Confirm the intended profiles are installed and selected and terminology validation is configured, then run `$validate`; structural validity is not clinical proof. | | Clinical meaning cannot be proven by counts | Use repeatable, reproducible random, and risk-based whole-chart samples. | | Exceptions remain | Track owner, severity, remediation, acceptance impact, and approval. | Do not prescribe a universal verification percentage. Choose samples after exact extraction counts are known and adjust for variability, risk, complexity, and defect history. #### Which load architecture meets throughput and recovery needs? Decide which writes must succeed or fail together, how the pipeline resumes, and how much throughput the cutover requires. Implementation details and project prerequisites are covered in [Building Migration Pipelines](/docs/migration/migration-pipelines). **Recommendation:** Use asynchronous batches for high-volume independent writes and synchronous transactions only for small groups that must succeed or fail together. Make every path safe to rerun, observable, and resumable. **Questions:** - Which writes are independent, and which small groups must succeed or fail together? - Which import path and measured throughput fit the data shape and cutover window? - How are results, checkpoints, retries, and rejected resources recorded? - Which downstream automations run, and what thresholds pause the load? For automations that remain active during the load, verify tenant scope and idempotency, and define how missed or failed work is recovered. Apply [Planning Automations](/docs/subscriptions#planning-automations) to these subscriptions; a successful import does not establish that its downstream side effects succeeded. **Output:** Choose a load path, define groups that require all-or-nothing writes and source-of-truth boundaries, and document recovery and side-effect handling in the [pipeline design](/docs/migration/migration-pipelines). ### Cutover and rehearsal #### When does write authority change, and how are later changes handled? Decide when Medplum becomes authoritative and prevent two systems from independently accepting authoritative writes. See [Phased Adoption and Cutover](/docs/migration/adoption-strategy). **⚠ One-way door:** A cutover is unsafe when two systems accept authoritative writes for the same domain without deterministic synchronization and conflict ownership. **Recommendation:** Choose the smallest cutover unit that preserves one clear write authority and can be rehearsed end to end. Use a phased transition only when domains, sites, or cohorts can be separated without ambiguous ownership. **Questions:** - Is transition big bang, phased, parallel, or backfill-only? - Which system owns reads and writes for each domain and phase? - How are changes since the previous extract captured, and can any path bypass the authoritative system? - How are dual-write failures and conflicts handled? - What evidence triggers go-live, pause, rollback, and legacy retirement? | Situation | Approach | | :-------------------------------- | :---------------------------------------------------------------------------------------------- | | Small, low-change source | Freeze, extract the final changes, load, verify, and switch. | | Higher operational risk | Prefer phased transition by domain, site, tenant, or user cohort. | | Baseline plus incremental changes | Record the last source change processed and use overlapping windows that are safe to run again. | | Temporary dual write | Keep one authoritative path, capture failures durably, and reconcile continuously. | | Medplum reads start first | Use feature flags and compare behavior while writes remain in the source. | | Legacy becomes an archive | Make it read-only, test access, and disable remaining write integrations. | **Output:** Complete the [Authority and Coexistence Matrix](/docs/migration/migration-planning#authority-and-coexistence-matrix). #### What must testing and dress rehearsals demonstrate? Decide how closely testing must match production volume, infrastructure, access, and workflow behavior. See [Testing and Accepting Migrated Data](/docs/migration/testing-and-acceptance). **Recommendation:** Run repeatable mapping tests and rehearse interruption, recovery, reconciliation, access, workflows, and rollback. Use production-scale data and infrastructure when volume, performance, or the cutover window creates material risk. **Questions:** - Which representative data and known edge cases can testing safely use? - Does the test environment match production features, access, integrations, and scale? - Have safe reruns, interruption, recovery, throttling, and partial failure been tested? - Have users rehearsed workflows, documents, cutover, and rollback? | Situation | Approach | | :----------------------------------------- | :-------------------------------------------------------------------------------- | | Deterministic mapping | Unit test normal, null, malformed, boundary, repeated, and unsupported cases. | | Unknown source variation | Profile production-like data and add each discovered defect to regression tests. | | Strict cutover window | Rehearse full volume on production-like infrastructure and measure every stage. | | Pipeline can be interrupted | Stop and resume during rehearsal; verify checkpoints and counts. | | Clinical workflows depend on migrated data | Run user acceptance tests with representative patient journeys. | | Access varies by role or tenant | Test allowed and denied access using actual migrated references and compartments. | A small pilot does not prove production throughput. A full-volume load does not prove clinical meaning. Test both when those risks apply. ### Protection and operational handoff #### How will migration data, credentials, and audit evidence be protected? Decide how protected data is extracted, staged, transmitted, logged, retained, and destroyed. See [Access Policies](/docs/access/access-policies) and [HIPAA Compliance](/docs/compliance/hipaa). **Recommendation:** Use dedicated least-privilege credentials, approved encrypted storage, PHI-minimized logs, and explicit retention and disposal rules for every temporary artifact. **Questions:** - Which people, systems, and environments may access migration PHI? - How are extracts, credentials, logs, rejected records, and reports protected? - What audit, retention, revocation, and secure-disposal rules apply? - What incident process handles a wrong-patient, wrong-tenant, or wrong-project load? | Situation | Approach | | :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ | | Production automation | Use a dedicated `ClientApplication` and narrowly scoped `AccessPolicy`, not a personal token. | | Extracts contain PHI | Use approved encrypted storage, restrict access, and destroy temporary copies after acceptance. | | Logs contain source data | Minimize fields and apply regulated-data access and retention controls. | | Production-like test data is needed | Prefer synthetic or de-identified data unless approved controls authorize PHI. | | Multi-tenant target | Define the tenant `AccessPolicy`; populate each resource's own patient references and/or governed `meta.account`; test allowed and denied access. | | Wrong-scope load | Stop the pipeline, preserve evidence, assess exposure, and use a reviewed correction plan. | Security review covers the pipeline and temporary artifacts, not only final FHIR resources. #### How will documents, binaries, and complex data remain usable? Decide whether complex data becomes structured resources, binary content, metadata, external references, or a separate workstream. **Recommendation:** Treat each complex domain as its own scoped workstream. Do not load it until its FHIR model, content storage, access rules, integrity checks, and retrieval workflow are approved. **Questions:** - Which complex domains are in scope, and must they remain operational or only viewable? - Are original bytes, structured data, searchable metadata, or all three required? - How are identity, clinical context, access, integrity, and retrieval verified? - Which unsupported or ambiguous data needs human review or legacy retention? Use [Binary Data](/docs/fhir-datastore/binary-data), the [Messaging Data Model](/docs/communications/messaging-data-model), and the [DICOM Data Model](/docs/dicom/data-model) rather than defining those models inside the migration plan. #### Who monitors runs, resolves exceptions, and accepts operational ownership? Decide how production runs are controlled and when migration ownership transfers to normal operations. **Recommendation:** Assign service ownership before production. Keep exceptions visible through resolution, define automatic pause thresholds, and close the migration only after operational handoff and temporary-access cleanup. **Questions:** - Which metrics, alerts, and thresholds pause a run? - Who owns technical, identity, clinical, and terminology exceptions? - Does the pipeline end or become an ongoing production integration? - What ends the post-go-live support period and permits credential, staging, and source-access retirement? | Situation | Approach | | :--------------------------------- | :------------------------------------------------------------------------------------------------------------- | | Finite migration | Define completion, archive evidence, revoke credentials, and dispose of temporary data. | | Ongoing synchronization | Treat it as a production integration with service ownership, alerts, runbooks, and reconciliation cadence. | | Transient failure | Retry with bounded backoff from a durable checkpoint. | | Repeatable mapping or data failure | Hold the record for review, fix the mapping or data, add a regression test, and safely rerun affected records. | | Identity ambiguity | Route to the identity-review owner, not a generic retry queue. | | Clinical ambiguity | Route to the clinical or terminology owner. | | Error threshold exceeded | Pause the affected stream, preserve state, and resume only after approval. | | Users need new workflows | Assign training, communications, support, and readiness owners before cutover. | ### Before You Implement Record: - Data scope, history boundaries, and exclusions - Identifier, matching, and mapping decisions - Load path, recovery approach, and operational owner - Write authority, cutover approach, and rollback criteria - Validation evidence, acceptance thresholds, and approvers - Security controls and unresolved risks Do not begin production migration work while any decision that changes identity, clinical meaning, write authority, or acceptance remains unowned or unresolved. --- ## Diagnostic Orders Decision Guide Use this guide to choose how your application orders diagnostic services, receives results, and coordinates follow-up. Each decision connects your requirements to a data model, integration approach, and operational responsibility, with links to implementation details. _Companion to [Labs and Imaging](/docs/labs-imaging), [Order Labs and Imaging](/docs/labs-imaging/ordering-labs-imaging), and [Results and Review](/docs/labs-imaging/results-and-review)._ The review queues and follow-up rules are application patterns to implement with Medplum resources. Confirm each facility's supported operations before committing to a workflow. Choose the decisions that match your application's role: 1. **[Choose connectivity](#how-will-systems-exchange-data).** Define which systems exchange orders and results and which interfaces they support. 2. **[Define ordering and release](#ordering-and-release)** if your application places orders. Decide who owns the catalog and what must happen before release. 3. **[Plan fulfillment and exceptions](#fulfillment-and-exceptions)** if you coordinate specimen collection, appointments, or incomplete orders. 4. **[Define result matching and display](#matching-and-displaying-results).** Match results to patients and orders and choose the detail users need. Include image storage, viewers, and context synchronization when displaying diagnostic images. 5. **[Assign review and closure](#review-and-closure).** Define follow-up, access, and responsibility for operational failures. Results-only applications can focus on connectivity, results, and review; add ordering and fulfillment if you coordinate them. External ordering and internal testing need all five areas. For internal testing, separately scope the system that performs the tests; for lab-only workflows, skip imaging details. Use the [questions below](#before-you-start) to establish your use case, then follow the relevant sections. Add requirements beyond these examples where needed. ### Before you start #### Which diagnostic services are in scope? - Laboratory tests sent to external labs - Tests performed in your own facility or at the point of care - Imaging studies performed externally or internally - A combination *Why it matters: laboratory workflows coordinate specimens; imaging workflows usually coordinate appointments and access to reports or images. An internal laboratory also needs catalog and laboratory operations design beyond sending orders.* #### Which parts of the workflow will your application own? - Ordering and receiving results - Receiving and reviewing results ordered elsewhere - Coordinating fulfillment of orders created elsewhere - Performing tests and producing results *Why it matters: a results-only application needs patient matching and review routing even when it has no original order. An ordering application also needs transmission, cancellation, and outstanding-order tracking.* #### Who participates, and who is accountable? Identify who can prepare and authorize orders, collect specimens or arrange appointments, resolve integration exceptions, review results, cover absent clinicians, and communicate with patients. Decide whether those responsibilities vary by organization, location, or service line. *Why it matters: the ordering clinician, performing organization, result reviewer, and follow-up owner may be different people. Unassigned work needs an explicit fallback.* #### Which systems are already involved? List the performing labs and imaging facilities, existing interfaces, vendor accounts, scheduling systems, and image archives. For each, identify what it can send and receive: orders, acknowledgments, cancellations, status updates, structured results, documents, or image access. *Why it matters: the application's workflow must reflect the capabilities of each connected facility. Connectivity to one laboratory does not establish imaging support or identical behavior across every facility.* ### How will systems exchange data? Choose an integration path for each facility. Confirm ordering, acknowledgments, cancellations, status updates, and result delivery separately; capabilities vary by destination. | Situation | Approach and downstream implications | | --- | --- | | Medplum Health Gorilla integration | Adopt its order profiles, send/receive Bots, and account requirements. Confirm facility coverage and assign integration exception ownership. See [Health Gorilla](/docs/integration/health-gorilla). | | Direct lab or imaging interface | Own the HL7v2 or API mapping, identifiers, acknowledgments, corrections, and recovery in each supported direction. See [HL7 interfacing](/docs/integration/hl7-interfacing). | | Internal testing | Create operational orders and results in your application; define which laboratory or imaging system owns performance and reporting. | | Requisition or manual handoff | Track handoff and acceptance separately, with an owner to reconcile completion. A generated requisition does not establish acceptance. | | Results only | Preserve source identifiers and support reports without local originating orders. Build patient matching and review routing ([Result matching](#how-will-incoming-results-find-the-correct-patient-and-order), [Result display](#what-result-detail-must-the-application-display-and-use), [Review and follow-up](#what-closes-the-loop-after-results-arrive)). | ##### Do you need an on-site protocol bridge? - **Local HL7/MLLP, ASTM, or DICOM equipment:** Use the [Medplum Agent](/docs/agent) for supported protocols. Your team operates the local host and connectivity, and implements workflow mapping and reconciliation. For imaging, follow the [DICOM channel setup](/docs/dicom/agent-dimse). - **HTTPS API or DICOMweb source:** Connect directly without an Agent. Your integration still owns mapping, reconciliation, and recovery. See the [DICOMweb API](/docs/dicom/dicomweb-api) for image ingestion. Name the authoritative system for the order, fulfillment status, and report, and define which updates may flow back. ### Ordering and release #### What can clinicians order, and who maintains the menu? First choose who owns the catalog, then decide how selections become patient-specific orders. ##### Who owns the catalog? | Choice | Data model and maintenance implications | | --- | --- | | Performing lab's menu | Preserve its orderable codes and required questions; assign someone to maintain vendor changes. A standardized observation code alone may not identify an orderable test. | | Internally maintained catalog | Define measurements with `ObservationDefinition`, specimen requirements with `SpecimenDefinition`, orderable services/panels with `PlanDefinition`, and procedures with `ActivityDefinition`. Your team owns those definitions. See [Diagnostic Catalog](/docs/careplans/diagnostic-catalog). | If clinicians also need reusable order sets, keep those definitions separate from the patient-specific `ServiceRequest` instances they generate and assign an owner for changes. ##### How will selections become orders? | Choice | Order structure | | --- | --- | | One orderable panel | Can be one requested service with multiple result observations. Follow the destination's panel definition. | | Several independent services | Create separate `ServiceRequest` resources; group them with a shared `requisition` when appropriate. `basedOn` identifies a request being fulfilled, rather than an arbitrary selection group. | | Health Gorilla ordering | Use its parent order and individual test `ServiceRequest` structure, linked through `basedOn`. Some labs require separate requisitions. See [Health Gorilla order structure](/docs/integration/health-gorilla/sending-orders#order-structure). | Preserve source codes and use verified terminology mappings. Flag unresolved mappings for review. #### What must happen before an order can be released? **Questions:** Can staff prepare orders for a clinician? Are orders placed within an encounter or independently? Which demographics, indications, Ask on Order Entry (AOE) answers, billing details, and supporting documents does the destination require? Represent the request with `ServiceRequest`, including patient, requester, service, and performer when known. Link the encounter when applicable. If preparation precedes authorization, use `status = draft` with `intent = order`, and define who can release it. | Requirement | Design consequence | | --- | --- | | Staff prepare; clinician authorizes | Give those roles distinct permissions and actions. Define the authorization record and who can trigger transmission. | | Test-specific AOE | Capture the destination's required answers. The Health Gorilla pattern stores `QuestionnaireResponse` resources in the individual test's `supportingInfo`. | | Insurance, patient, or account billing | Capture the billing route and required supporting information before sending. For Health Gorilla, check practitioner and practice lab account requirements. | | Imaging requires authorization or scheduling coordination | Assign that work explicitly and decide whether it must finish before transmission, scheduling, or performance. | Track saving, authorization, and transmission separately. Follow the chosen integration's lifecycle rather than treating a saved order as successfully sent. References: [Order Labs and Imaging](/docs/labs-imaging/ordering-labs-imaging), [Health Gorilla Sending Orders](/docs/integration/health-gorilla/sending-orders). ### Fulfillment and exceptions #### How will the patient or specimen reach the performing facility? **Questions:** Who collects the specimen? Who schedules imaging? Are home kits involved? How will staff identify missing collections, missed appointments, rejected specimens, or incomplete kits? | Situation | Recommended approach | | --- | --- | | Specimen collected on site | Record the actual `Specimen`, collection details, and order linkage. Include requisition and label handling in the workflow. | | Collection at an external draw station | Give the patient the destination and preparation instructions; define what evidence confirms collection and who follows up when it is missing. | | Home collection | Track kit fulfillment, return, receipt, and rejection as operational work. A shipped kit does not establish specimen collection or completed testing. | | Imaging at another facility | Track the appointment handoff and expected completion. Use `Appointment` if your application manages scheduling; otherwise reconcile the external appointment status. | | Multiple specimens or collection times | Preserve the individual specimen relationships and independent progress. Use the integration's order-splitting behavior when required. | **If you only display clinical order state,** use `ServiceRequest`. **If staff need assignments, deadlines, or escalation,** add `Task`, with `focus` on the order and `for` referencing the patient. Track that work separately from order status. See [Tasks and Work Queues](/docs/careplans/tasks). #### What happens when an order changes or never completes? **Questions:** Can orders be edited after transmission? How is cancellation acknowledged? What happens after a rejected specimen, missed appointment, or overdue result? Are recurring orders in scope? | Situation | Recommended approach | | --- | --- | | Draft changes | Edit before release and revalidate destination requirements. | | Change after transmission | Follow the destination's amendment rules. If replacement is required, coordinate cancellation and link the replacement with `ServiceRequest.replaces`. | | Cancellation | Use `revoked` for the cancelled clinical order, while separately tracking whether the external destination acknowledged the cancellation. | | Order entered by mistake | Use `entered-in-error`; preserve the history needed to understand what happened. | | Overdue or failed fulfillment | Create an assigned exception with a next action. Define when staff retry, contact the patient or facility, cancel, or request a new order. | | Destination supports a standing order | Capture the authorized timing and validity, and track each performance and report independently. Agree how results identify the standing order and occurrence. | | Each occurrence requires a new order or requisition | Create separately authorized requests for the individual occurrences. Do not reuse a completed one-time order as a recurring work queue. | **Integration constraint:** Updating an active Health Gorilla order in Medplum does not modify it in the performing lab's system. Do not offer an edit action that implies an external change the integration cannot perform. After a send timeout, reconcile the destination's order identifier before resending. Preventing duplicate FHIR resources does not prevent duplicate external orders. References: [Order lifecycle](/docs/integration/health-gorilla/sending-orders#order-lifecycle), [Cancelling and Amending Orders](/docs/labs-imaging/ordering-labs-imaging#cancelling-and-amending-orders). ### Matching and displaying results #### How will incoming results find the correct patient and order? Choose the identifiers used to match patients, orders, and report revisions. Preserve their source namespaces; one order may produce multiple reports or deliveries. | Situation | Data model and workflow implications | | --- | --- | | Known patient and order | Match on agreed identifiers and link known orders through `DiagnosticReport.basedOn`, which can reference multiple orders. Retain source identifiers for reconciliation. | | Known patient, no local order | Store the report without inventing an order relationship; `basedOn` is optional. Assign review ownership. | | Unknown or ambiguous patient | Route for identity resolution. Check whether the connector creates patients automatically and define how those records are reconciled. | | Duplicate delivery or replay | Use conditional create/update keyed on stable source identifiers. Distinguish repeated deliveries from report revisions and prevent older results from overwriting newer ones. See [Working with FHIR](/docs/fhir-datastore/working-with-fhir). | | Missed delivery | Provide reconciliation and backfill, with an owner for unresolved exceptions. | **Health Gorilla implication:** Its receiver flags unsolicited reports with `DetectedIssue`. An unmatched patient causes it to create a patient from the supplied demographics and flag an `unknown-patient` issue. Plan a resolution queue; see [Receiving Results](/docs/integration/health-gorilla/receiving-results#resolving-orders-with-results). #### What result detail must the application display and use? Choose the formats your users need. Structured values, original reports, and images can coexist; each adds different modeling and integration work. | Need | Data model and downstream implications | | --- | --- | | Discrete values, trends, or rules | Store `Observation` values with units, source reference ranges, interpretation, and timing. Validate comparability across sources. | | Report summary and lifecycle | Use `DiagnosticReport`, link observations through `result`, and preserve partial, preliminary, final, amended, and corrected states. See [Results and Review](/docs/labs-imaging/results-and-review). | | Original PDF or narrative is sufficient | Preserve it through `presentedForm` or the integration's `DocumentReference` relationship. Discrete observations are unnecessary unless the application needs them. See [Binary Data](/docs/fhir-datastore/binary-data) for file storage and access. | | PDF-only source, but discrete results are required | Add extraction, verification, and source provenance before using extracted values for trends or rules. | | Imaging interpretation only | Display the narrative or attached radiology report; image storage and viewer integration can remain out of scope. | | Diagnostic images | Choose archive/viewer ownership below, then decide whether applications need synchronized context. | ##### Where will images live, and how will clinicians view them? - **Medplum DICOM storage and hosted OHIF viewer** - **Choose when:** You need image storage and viewing without deploying a separate image server and viewer. - **Implications:** Implement ingestion, patient/order reconciliation, and image-access policies. Medplum's custom DICOM resources are distinct from R4 `ImagingStudy`; ingestion does not automatically link a FHIR patient or create an `ImagingStudy`. Create those relationships when required by your workflow. - **Tradeoffs:** DICOM support is beta and implements a subset of DICOMweb. Validate modality and viewer compatibility. Self-hosted OHIF deployment is an enterprise feature. - **Implementation:** [DICOM](/docs/dicom), [Data Model](/docs/dicom/data-model), [OHIF Viewer](/docs/dicom/ohif-viewer). - **Existing PACS or external viewer** - **Choose when:** A facility already owns image storage and interpretation. - **Implications:** Agree viewer launch, authentication, study identifiers, and image availability. Link reports through `DiagnosticReport.imagingStudy` when using R4 `ImagingStudy`. - **Tradeoffs:** Avoids a second archive, but a FHIR reference alone does not retrieve images or launch the viewer; integration with the facility is still required. ##### Should the chart and viewer switch context together? **If clinicians need synchronized patient or study selection,** evaluate [FHIRcast](/docs/fhircast). Medplum supports FHIRcast STU3 and OHIF context synchronization. Define participating applications, a shared session/topic, and supported events. This can accompany either storage choice when the applications support it. **If independent navigation is sufficient,** skip context synchronization. FHIRcast does not replace image retrieval, authentication, or persisted order/result records. ### Review and closure #### What closes the loop after results arrive? **Questions:** Who reviews routine and critical results? Who covers absences? When are patients notified? How are follow-up actions tracked? Does a correction require another review? **Recommended baseline:** Keep report finality, clinician review, patient communication, and follow-up completion distinct. `DiagnosticReport.status = final` describes the report; it does not mean a clinician has reviewed it or contacted the patient. | Requirement | Suggested application pattern | | --- | --- | | One reviewer, no separate queue required | Record acknowledgment and the report version reviewed in your clinical workflow, for example with `Provenance`. Do not change the report's clinical status to mean reviewed. | | Assigned work, coverage, or overdue review | Create a `Task` focused on the report, with the patient in `Task.for`. Record ownership, deadlines, reviewer, review time, and report version. See [Tasks and Work Queues](/docs/careplans/tasks). | | Covering clinician or unassigned result | Route through a defined coverage rule and fallback queue. Avoid leaving ownership implicit in the ordering clinician field. | | Critical-result escalation | Implement the clinical team's escalation and acknowledgment policy, including coverage and unresolved alerts. An abnormal flag is input to that workflow. | | Patient communication | Define release and communication rules with the responsible team. Track communication separately from reviewing the result. | | Follow-up testing, visit, or referral | Create the appropriate follow-up resource and assigned work; define whether it must complete before the original review work closes. | | Amended or corrected report | Reopen or create review work according to the correction policy, retaining which earlier version was reviewed and communicated. | If routing is automated, use narrowly scoped Subscriptions and Bots. Handle report creation and meaningful updates, even when status is unchanged; conditionally create review work so repeated deliveries do not duplicate tasks. See [Subscription Extensions](/docs/subscriptions/subscription-extensions). #### Who can access the data and resolve operational failures? **Questions:** Which organizations share patients? Can ordering and covering clinicians see the same results? What can collection staff, integration services, and patients read or change? Who owns failed sends and unresolved matches? Use `AccessPolicy` and `ProjectMembership` to enforce the chosen access boundaries; see [Access Policies](/docs/access/access-policies). Give each patient-specific resource its own applicable patient reference; access does not propagate merely because a resource links to an order or report. Include documents and their Binary security context in that design. For Medplum DICOM storage, explicitly design and test access to the DICOM resources and image binaries. Matching a DICOM patient ID or creating an `ImagingStudy` link does not automatically establish patient-compartment access for the stored images. Define separate operational ownership for transmission failures, identity exceptions, overdue fulfillment, and unreviewed results. Each queue needs an owner and a resolution action. Reference: [Access Control Decision Guide](/docs/decision-guides/access-control). ### Before You Implement Record these decisions in the implementation plan: - Services, facilities, integration paths, and supported message types. - Catalog owner, order grouping, ordering permissions, required information, and release rules. - Collection or scheduling responsibilities and completion evidence. - Identifier mapping, result formats, matching, amendment/cancellation rules, recurring orders, and recovery. - Image storage, viewer, context synchronization, and image-access boundaries when in scope. - Review, coverage, patient communication, and follow-up ownership. - Access boundaries and the definition of a closed diagnostic workflow. Walk through a routine lab order, an imaging order, a rejected or cancelled order, an unmatched result, a corrected report after review, and an absent reviewer. The design should make the next action and its owner clear in each case. --- ## E-Prescribe Decision Guide Use this guide to choose an e-prescribing integration and plan the workflow around it. Work through hosted versus custom prescribing workflows, prescriber enrollment, controlled substances, and practice structure to define what your application builds, what the vendor handles, and what must be ready before go-live. _Companion to the [E-Prescribe (eRx)](/docs/medications/e-prescibe) docs._ E-prescribing in Medplum is part of the [Medplum EHR](/solutions/medplum-ehr), not a standalone product. It runs through an integration with a vendor connected to the e-prescribing network. See [Related Integrations](#related-integrations) for the available first-party options. Choose the prescribing experience, then plan what supports it: 1. **[Choose hosted or custom prescribing](#hosted-or-custom-prescribing).** Decide which workflow your application builds and which interface the integration provides. 2. **[Plan enrollment and staff roles](#enrollment-and-staff-roles).** Define onboarding, identity requirements, supervision, and evidence that each user is ready to prescribe or assist. 3. **[Plan EPCS enrollment](#how-will-epcs-enrollment-work)** if prescribers will send controlled-substance prescriptions. 4. **[Define prescribing and follow-up](#prescribing-and-follow-up).** Identify required benefits and authorization checks, refills, presets, pediatric workflows, and medication history. 5. **[Define practice structure and transition](#practices-and-transition).** Choose practice context; address multiple locations, independent clients, or migration from an existing system where applicable. Use the [questions below](#before-you-start) to establish your use case, then follow the relevant sections. Add requirements beyond these examples where needed. ### Before you start #### What are you prescribing? - Non-controlled medications only - Controlled substances too (Schedule II–V) - Not sure / depends on specialty *Why: controlled substances require **EPCS**, which adds a per-prescriber enrollment path (DEA, identity proofing, two-factor auth). If you never prescribe them, skip the controlled-substance decision ([EPCS](#how-will-epcs-enrollment-work)) and enrollment shrinks substantially.* #### Do you want the hosted flow, or a custom workflow? - **Hosted iframe** — embed the vendor's prescribing screens. Drug search, dosing, pharmacy, and send all happen inside the vendor's flow. - **Custom workflow** — prescribe from the chart using Medplum's FHIR operations and bots. Only the final, regulation-required **review-and-send** step stays a hosted widget. - Not sure *Why: the iframe is fastest to live, and the steps stay the vendor's. A custom workflow keeps prescribing in the chart. Presets — clinic favorites and order sets — standardize repeat orders on either path. Either way, transmission stays on the vendor's review-and-send step. See [Prescribing workflow](#hosted-or-custom-prescribing).* #### New build or replacing an existing e-Rx system? - New — no e-prescribing today - Replacing another EHR or e-Rx vendor - Adding alongside an existing clinical system *Why: replacing an existing system requires coordinating a **Change of Vendor** with the prescribing network so refills and history follow prescribers over, a distinct migration workstream ([Transition](#how-will-prescribers-transition-from-an-existing-e-rx-system)).* #### Who is involved in prescribing? - Prescribers (sign and send) - Clinicians prescribing under supervision - Non-prescribing proxies or staff who stage orders - Administrators (enroll prescribers, manage practices, configure favorites) - Any offshore or non-US staff using e-prescribing? *Why: enrollment and permissions depend on the vendor and role ([Identity and roles](#what-identity-data-roles-and-supervision-relationships-are-required)). Supervised prescribing and non-prescribing order preparation are different responsibilities. Confirm which roles can stage, sign, send, or administer, and the vendor's requirements for non-US staff before designing the workflow.* ### Hosted or custom prescribing? Whether prescribing follows the vendor's hosted flow or a custom workflow in the chart. This choice shapes almost every section below. **Questions:** - Do prescribers write prescriptions from inside the chart, or is the vendor's hosted flow enough? - Do you want presets — clinic favorites and order sets — for medications the practice sends often? - Is the vendor's standard sequence of steps acceptable? | Situation | Approach | | :---- | :---- | | Hosted flow is enough | **Hosted iframe** — the vendor's screens, embedded. Fastest to live; the steps are the vendor's. | | Prescribe from the chart | **Custom workflow** — stage the order with FHIR operations and bots; the prescriber stays in the chart until the final send. | | Repeat medications | **Presets** — clinic favorites and order sets ([Presets and pediatrics](#are-favorites-order-sets-or-pediatric-workflows-needed)). These are configuration, not a separate prescribing interface. | | Either path | The final review-and-send step is always vendor-hosted and compliance-controlled. A custom workflow does not replace transmission. | > **⚠ Choose deliberately.** This is expensive to reverse — it determines whether prescribers stay in the chart, and how enrollment and presets surface. Align on [Hosted or custom workflow](#do-you-want-the-hosted-flow-or-a-custom-workflow) before you commit to a workflow. ### Enrollment and staff roles #### Who drives enrollment, and how is readiness tracked? Every prescriber must be enrolled before transmitting. Choose who drives enrollment and how much your platform automates. Map the selected integration's states to actionable progress; an invitation, account registration, and readiness to prescribe are different milestones. **Questions:** - Who initiates enrollment — an ops admin, or the prescriber on first login? - Do prescribers send controlled substances (which adds identity proofing and 2FA — see [EPCS](#how-will-epcs-enrollment-work))? - How will you see which prescribers are stuck mid-enrollment? | Situation | Approach | | :---- | :---- | | Admin-driven enrollment | Use the integration's supported administrative workflow. Define caller permissions, required credentials, and which steps still need prescriber participation | | Provider self-service | Choose a supported invite or embedded enrollment flow. Track completion and link the vendor user account to the Medplum user; launching the flow alone is not completed enrollment | | Bulk onboarding or profile updates | Confirm support for administrative sync, activation, and network registration. Decide who approves changes and handles failed or incomplete records | | Non-controlled only | Complete the requirements for non-controlled prescribing; track additional EPCS enrollment separately if controlled substances are later added | | Track enrollment state | Show progress, errors, the next required action, and its owner. Keep basic registration and EPCS readiness distinct | #### What identity data, roles, and supervision relationships are required? Accurate provider identity data prevents the most common enrollment failures. **Questions:** - Do Practitioner records satisfy the selected vendor's required profile and role? - Do controlled-substance prescribers have a valid DEA number with issuing state? - Which roles do users need: independent prescriber, supervised prescribing clinician, non-prescribing proxy, or admin? - Who validates and maintains supervising-prescriber relationships for each clinic? - Do non-prescriber staff (front desk, MAs) need to perform prescribing-adjacent tasks — searching pharmacies, setting a preferred pharmacy, viewing history — without an NPI? - Are any of those staff offshore or otherwise outside the US? | Situation | Approach | | :---- | :---- | | Prescriber profile | Populate the selected integration's required Practitioner fields and identifiers. Validate demographic, contact, NPI, and license data before enrollment; required fields vary by vendor | | Controlled-substance prescriber | Complete the selected vendor's EPCS requirements, including its required DEA data and issuing-state representation | | Staff who stage but do not prescribe | Use a non-prescribing role and confirm its permitted actions. Order preparation must not imply authority to sign or send | | Clinician requiring supervision | Confirm supported prescribing and supervising roles, then configure the relationship for the intended clinic. Assign ownership for eligibility checks, updates, and removal | | Users who enroll others | Provision the required administrative role and credentials to manage prescribers and favorites ([Presets and pediatrics](#are-favorites-order-sets-or-pediatric-workflows-needed)); confirm the scope of that access | | Non-prescriber staff running pharmacy/history ops | Confirm which operations staff need and whether each requires a vendor account, identifier, or additional permission. Provision the supported non-prescribing role | | Offshore / non-US staff | Confirm permitted activities, enrollment requirements, and administrative access with the selected vendor before designing the staffing model | Validate both clinicians' eligibility before assigning a supervisor, and confirm what the integration checks itself. Successfully linking enrolled accounts does not by itself establish that the relationship is appropriate for the intended clinic. ### How will EPCS enrollment work? *Read only if [Medication scope](#what-are-you-prescribing) includes controlled substances. Otherwise skip to [Benefits and authorization](#will-prescribers-need-benefit-checks-and-prior-authorization).* Supported, but a regulated, multi-step enrollment on top of basic registration. Treat it as its own workstream. **Questions:** - Which prescribers will send controlled substances, and does each have a valid, active DEA registration (with state)? - Are prescribers ready to complete identity proofing and set up two-factor auth? - Have any prescribers previously enrolled in EPCS with this vendor under another organization? | Situation | Approach | | :---- | :---- | | EPCS enrollment | Follow the selected vendor's registration, required agreements, identity proofing, and two-factor activation workflow. Track completion before allowing controlled-substance prescribing | | Readiness check | Confirm EPCS activation for each prescriber in the selected integration. Stage names and identity-proofing providers vary; basic enrollment is not proof of EPCS readiness | | Missing DEA data | Resolve the selected vendor's required DEA data and state information before EPCS enrollment ([Identity and roles](#what-identity-data-roles-and-supervision-relationships-are-required)) | | Prior enrollment elsewhere | Can cause verification conflicts (e.g. reused phone number). Identify these prescribers up front so vendor support can resolve them before onboarding. | > **⚠ EPCS is a one-way gate on go-live.** Identity proofing and 2FA are per-prescriber, take real calendar time, and are the most common onboarding delay. Start early and build admin visibility into each prescriber's stage ([Enrollment](#who-drives-enrollment-and-how-is-readiness-tracked)). Controlled-substance prescriptions also require effective dates and often a diagnosis — confirm your workflow captures these. ### Prescribing and follow-up #### Will prescribers need benefit checks and prior authorization? Whether prescribers see coverage and cost at the point of prescribing, and how prior auth is handled. **Questions:** - Do prescribers need formulary status, patient cost, and covered alternatives while prescribing? - Do you need to initiate electronic prior authorization (ePA) — through the eRx vendor, the standards-based payer path, or both? - Is your timeline driven by upcoming e-prescribing mandates? | Situation | Approach | | :---- | :---- | | Show coverage & cost | The vendor pulls active benefit info from pharmacy-benefit data via patient demographics and surfaces formulary status, cost, and alternatives. Coverage comes from the benefit network, **not** insurance data stored only in Medplum, and prescription cost is **not** retrievable via the Medplum API — keep demographics accurate so the match succeeds. | | ePA via the eRx vendor | The e-prescribing vendor can initiate ePA from the medication when formulary data flags it's required. This may require **separate enablement** with the vendor rather than being part of the base integration — confirm it's in scope. | | Standards-based ePA (payer path) | The emerging FHIR path for payer-connected prior auth — CDS Hooks with CRD / DTR / PAS (ONC criteria (g)(31)–(g)(33)). Currently [alpha](/docs/integration/electronic-prior-auth). | | Compliance-driven timeline | HTI-4 (effective Oct 1, 2025) mandates **NCPDP SCRIPT v2023011** with ePA in the prescribing workflow and **RTPB v13** for real-time cost display. Payer ePA APIs are due **Jan 1, 2027**; eRx modules must be certified to SCRIPT v2023011 by **Jan 1, 2028**. Factor these into [Prescribing workflow](#hosted-or-custom-prescribing). See [HTI-4 & CMS-0057-F](/docs/compliance/hti-4). | #### How will refills, reorders, and discontinuation be handled? How prescribers handle prescriptions after the first send. **Questions:** - Do prescribers respond to pharmacy-initiated refill requests? - Do they re-prescribe previously prescribed medications? - Do you need to discontinue or expire prescriptions — automatically on change? | Situation | Approach | | :---- | :---- | | Refills | Pharmacy-initiated requests for more of an existing prescription; prescribers approve or deny. | | Reorders | Prescriber-initiated new prescription for a previously prescribed medication. | | Discontinuation / expiry | Manual today — no automatic discontinue-on-change or auto-expire. High-polypharmacy populations should plan an explicit medication-reconciliation workflow. | #### Are favorites, order sets, or pediatric workflows needed? **Questions:** - Does the practice repeatedly prescribe a common set of medications worth curating? - Do you prescribe to pediatric patients? | Situation | Approach | | :---- | :---- | | Common medication set | Configure supported **clinic favorites / order sets**, including specialty/compounded configurations where available. Confirm which administrative role may create and edit them ([Identity and roles](#what-identity-data-roles-and-supervision-relationships-are-required)). | | Highly variable prescribing | Skip favorites — the maintenance overhead isn't worth it. | | Pediatric patients | The **prescriber is responsible** for dosages within guidelines for the patient's height and weight; vendors often add pediatric validation — confirm requirements before go-live. | #### What medication history is needed, and how is consent captured? Whether prescribers can see a patient's external medication history, and the consent required to retrieve it. **Questions:** - Do prescribers need a patient's medication history from outside your system (pharmacy fill history, active prescriptions elsewhere)? - Can you capture and record the patient consent required to query external history? - Are you importing prescription history from a prior system? | Situation | Approach | | :---- | :---- | | Need external history | Confirm the integration's access to network medication history, including pharmacy fill history and/or active prescriptions. This is a distinct capability from writing prescriptions, so scope it explicitly. | | Consent gate | External history retrieval requires **patient consent**; capture and record it (e.g. a `Consent` resource) before querying, and confirm the consent language your workflow presents. | | Importing prior history | History migrated from a prior system generally loses prescriber/pharmacy linkage and may not appear in the vendor's native "past prescriptions" view — set expectations accordingly. | ### Practices and transition #### How will prescriptions resolve to the correct practice? Read when prescribers work across more than one location or legal entity. **Questions:** - Do prescribers prescribe on behalf of more than one practice? - How should the system know which practice a prescription belongs to? - Do you need a default pharmacy per patient or per clinic? | Situation | Approach | | :---- | :---- | | Multiple practices/locations | Model practices as `Organization` resources and prescriber affiliations with `PractitionerRole`. Map that structure to the vendor's practice hierarchy and enrollment requirements | | Resolving the practice per prescription | Select the intended practice explicitly when a prescriber has multiple affiliations. Confirm how the integration accepts that context and handles missing or ambiguous selections | | Default pharmacy | Decide whether pharmacy preferences belong to the patient or clinic, confirm supported behavior, and allow the correct pharmacy to be selected for each prescription | #### How will independent clients be isolated and administered? Read when one Medplum project serves multiple **downstream clients** that prescribe independently (a platform or enablement model) — distinct from one organization's own locations ([Practice selection](#how-will-prescriptions-resolve-to-the-correct-practice)). This is an architecture fork worth settling before the second client onboards. **Questions:** - Does a single Medplum project serve multiple downstream clients or brands that prescribe independently? - Must each client's prescribers and patients be isolated from the others? - Does the originating clinic's identity or branding need to reach the pharmacy? - Do downstream clients' admins need scoped self-service access to manage their own prescribers? | Situation | Approach | | :---- | :---- | | One organization, many sites | Not multi-tenant — use Multi-Location ([Practice selection](#how-will-prescriptions-resolve-to-the-correct-practice)). | | Platform serving independent clients | Model each downstream client as its own practice/tenant so prescribers, patients, and rosters are scoped per client rather than pooled. | | Roster / patient isolation required | Scope prescriber pickers and patient lists to the client's tenant — a single shared tenant can expose one client's prescribers or patients to another. Decide isolation before onboarding the second client. | | Origin branding at the pharmacy | If the medication must reach the pharmacy carrying the originating clinic's identity/branding, model a distinct tenant per client rather than one shared tenant. | | Scoped admin access | Confirm whether the selected hosted or custom workflow can restrict client admins to their own prescribers and patients. Define and verify those permissions in Medplum and the vendor system before granting access; include this in [Prescribing workflow](#hosted-or-custom-prescribing) | | Shared vs. per-client tenant | A single shared tenant is simplest but limits isolation and branding; a tenant-per-client isolates cleanly but adds enrollment and ops overhead (prescribers may need enrolling in each). Choose based on how much the clients must be kept apart. | #### How will prescribers transition from an existing e-Rx system? Read only when replacing an existing EHR or e-Rx vendor ([Existing systems](#new-build-or-replacing-an-existing-e-rx-system)). **Questions:** - Are prescribers currently sending through another system on the prescribing network? - Do pending refills and history need to follow prescribers over? - Can migration be phased prescriber-by-prescriber, or must it be all at once? | Situation | Approach | | :---- | :---- | | Already prescribing through another system | Coordinate the network's **Change of Vendor** process so the new system is recognized; without it, new prescriptions may work while refills and edits fail. | | Preserving pending refills | The approach depends on whether pending refills must be preserved vs. whether the old system must keep operating during cutover — these pull opposite ways. Scope with the vendor before scheduling. | | Phased rollout | Migration can generally be coordinated per prescriber, enabling a phased cutover. | ### Related Integrations See the integration docs for setup and implementation details: - [ScriptSure](/docs/integration/scriptsure): custom prescribing workflows using FHIR operations and Bots, with hosted review and send. - [DoseSpot](/docs/integration/dosespot): embedded prescribing through a hosted iframe. --- ## Decision Guides Use these guides to make application design, workflow, and rollout decisions before implementation. Each guide connects your requirements to recommended approaches, data models, and downstream responsibilities. Use the guides and technical docs together: the guide explains which approach fits your situation and what it requires; the linked technical docs provide configuration, APIs, and implementation examples. **[Plan your application with AI](/docs/building-with-ai-coding-assistants#plan-your-application):** Give your AI agent the guides, work through the questions together, and save the decisions in your application's repo for future development. Each guide includes a copyable planning prompt. ### Application Design Define the identity, tenancy, and access rules shared across your workflows. #### Authentication Choose account matching, identity providers, and token-flow architecture, including client or server token storage and user-scoped access versus On-Behalf-Of. - **[Decision Guide →](/docs/decision-guides/authentication)** - **Docs:** [Authentication](/docs/auth) #### Access Control and Tenancy Scope your authorization model – user types and organizational structure, multi-tenancy and patient/practitioner sharing, role inventory and permission mechanics, admin structure, and optional patterns like SMART on FHIR and IP access rules. - **[Decision Guide →](/docs/decision-guides/access-control)** - **Docs:** [Authorization and Access Control](/docs/access) ### Clinical and Operational Workflows Decide how people and systems carry out a clinical or operational process, including ownership, handoffs, and exceptions. #### Intake & Registration Scope your intake workflow – who completes it and when, identity and duplicate handling, form library, prefill and reconciliation, extraction approach (SDC vs. Bot), coverage and consent modeling, and post-submission automation. - **[Decision Guide →](/docs/decision-guides/intake)** - **Docs:** [Intake & Registration](/docs/intake) #### Scheduling Scope your scheduling workflow – visit types, actors and availability, patient preferences and eligibility, multi-resource booking, capacity, holds, appointment lifecycle, and integrations. - **[Decision Guide →](/docs/decision-guides/scheduling)** - **Docs:** [Scheduling](/docs/scheduling) #### Charting Scope your charting workflow – care setting and visit types, how structured the chart should be, parsing form responses into queryable data, signing requirements, and Visit Template ownership. - **[Decision Guide →](/docs/decision-guides/charting)** - **Docs:** [Charting](/docs/charting) #### Diagnostic Orders Choose how to order labs and imaging, coordinate fulfillment, match incoming results, and assign review and follow-up. Compare external interfaces, internal testing, and results-only workflows, including imaging storage and viewer choices. - **[Decision Guide →](/docs/decision-guides/diagnostic-orders)** - **Docs:** [Labs and Imaging](/docs/labs-imaging) #### E-Prescribe Scope your e-prescribing workflow – hosted iframe vs. custom workflow, controlled substances, prescriber enrollment and identity verification, and vendor integration approach. - **[Decision Guide →](/docs/decision-guides/e-prescribe)** - **Docs:** [Medications](/docs/medications) #### Referrals Scope your referral workflow – which sides of the lifecycle you build (send, receive, or both), referral semantics and coding, capture flow and recipient directory, what clinical context to carry, transmission channels, lifecycle and assignment, and closure round-trip. - **[Decision Guide →](/docs/decision-guides/referrals)** - **Docs:** [Referral Management](/docs/careplans/referrals) #### Care Coordination Scope operational workflows across the practice: top-of-license delegation, shared queues and ownership, handoffs and deadlines, longitudinal cases, care plans and goals, protocols, clinical decision support, and follow-through. - **[Decision Guide →](/docs/decision-guides/care-coordination)** - **Docs:** [Care Coordination](/docs/careplans) #### Messaging & Communications Scope your messaging workflow – thread structure, live updates, routing and assignment, read receipts, attachments, drafts and editing, automations, external channels (SMS, email), and async encounter billing. - **[Decision Guide →](/docs/decision-guides/messaging)** - **Docs:** [Messaging & Communications](/docs/communications) #### RCM & Billing Scope your billing workflow – charge capture, claims and clearinghouse integration, eligibility checks, coding and fee schedules, remittance and reconciliation, and patient billing. - **[Decision Guide →](/docs/decision-guides/rcm-billing)** - **Docs:** [Billing and Payments](/docs/billing) ### Implementation and Rollout Plan the transition to Medplum and how your team will validate and introduce the application into use. #### Data Migration Scope a migration to Medplum – source-system inventory, data ownership, identity and mapping governance, import operations, validation, testing, cutover, and acceptance. - **[Decision Guide →](/docs/decision-guides/data-migration)** - **Docs:** [Migrating to Medplum](/docs/migration) --- More guides will be added as additional workflow areas are scoped. If you'd like a guide for a workflow not listed here, reach out on our [Discord](https://discord.gg/medplum) or email [support@medplum.com](mailto:support@medplum.com). --- ## Intake & Registration Decision Guide Use this guide to design how patients, caregivers, and staff complete intake and registration. Work through identity matching, forms, prefill, data extraction, coverage, consent, and post-submission actions to define the information you collect and what happens when intake is complete. _Companion to the [Intake & Registration](/docs/intake) docs._ Make four decisions about the intake workflow: 1. **[Define patient identity and forms](#patient-identity-and-forms).** Decide when the patient is confirmed, who supplies the answers, and which capture flows are needed. 2. **[Choose prefill and extraction](#prefill-and-extraction).** Identify any prefill sources and conflict-resolution rules; choose how responses create or update structured records and how resubmissions behave. 3. **[Define coverage and consent](#coverage-and-consent)** where required. Decide how insurance relationships are represented and which consents must be captured or renewed. 4. **[Define completion and follow-up](#completion-and-follow-up).** Decide whether intake needs orchestration or encounter linkage, what counts as complete, and which actions run afterward. Use the [questions below](#before-you-start) to establish your use case, then follow the relevant sections. Add requirements beyond these examples where needed. ### Before you start #### Who completes intake, and in what setting? - Patient self-service (portal, mobile, kiosk) - Staff-assisted (registration desk, rooming, phone, paper-to-digital) - Mixed (patient starts online, staff verifies in person) *Why: determines the auth model and whether an intake response may exist before the patient record is created ([Patient identity](#when-is-intake-linked-to-a-confirmed-patient-record), [Forms and capture](#which-forms-and-capture-flows-are-needed)).* #### When does intake happen relative to identity and scheduling? - Before the patient record exists (greenfield / new patient signup) - After scheduling, pre-visit (patient exists, no visit yet) - At check-in or during the visit - Episodic updates only (existing patient, not tied to a visit) *Why: drives whether intake creates or updates a patient record, when responses get tied to a patient, and whether intake tasks link to a visit ([Patient identity](#when-is-intake-linked-to-a-confirmed-patient-record), [Orchestration](#does-intake-need-orchestration-and-encounter-linkage)).* #### How are returning patients and duplicates handled? - Always create a new patient record on intake (rare; only if you have a separate dedup pipeline downstream) - Look up by identifier (e.g. MRN, email, phone) and update if found - Run a demographic candidate search and confirm with staff before merge - Mixed – different rules for patient self-service vs staff-assisted *Why: drives [Patient identity](#when-is-intake-linked-to-a-confirmed-patient-record) and must align with your org-wide dedup policy.* #### What consents need to be captured at intake, and are any state- or population-specific? *Why: drives [Consent](#which-consents-are-required-and-when-must-they-be-renewed); sensitive categories may constrain what post-intake automation can auto-share or auto-route ([Post-submission actions](#which-actions-run-after-submission-and-how-do-failures-surface)).* #### What other systems must intake feed or read from? - Eligibility / insurance verification (e.g. Stedi, clearinghouse 270/271) - Practice management or legacy EHR (write-back of demographics or coverage) - Prefill sources (Patient Access API, Payer-to-Payer API, partner EHR, HIE/TEFCA) — see [Prefill](#where-does-prefill-come-from-and-who-resolves-conflicts) - Referrals (intake initiated by an inbound referral – see the Referrals guide) *Why: surfaces integration boundaries that affect prefill ([Prefill](#where-does-prefill-come-from-and-who-resolves-conflicts)), post-intake automation ([Post-submission actions](#which-actions-run-after-submission-and-how-do-failures-surface)), and which actions live inside one intake workflow vs separate workflows.* ### Patient identity and forms #### When is intake linked to a confirmed patient record? Decide whether intake creates a new patient record, updates an existing one, or runs unlinked until a human confirms a match – and how the intake response gets tied to the right patient. For matching rules, master records, and merge mechanics, defer to [Patient Deduplication Architectures](/docs/fhir-datastore/patient-deduplication) and [Patient `$match`](/docs/api/fhir/operations/patient-match). **Questions:** - Does the patient record exist before intake is filled out, after, or sometimes either? - Do you run an identifier lookup, a demographic match search, both, or neither before creating a new patient record? - When a match is ambiguous, does the intake response stay unlinked until a human confirms, or does the system pick the best candidate automatically? - For self-service flows, can a patient submit intake before authenticating, and how is that response later associated with an account? | Situation | Approach | | :---- | :---- | | Patient exists before intake (scheduled visit, returning patient) | Set `QuestionnaireResponse.subject` at create; intake Bot updates the existing Patient. | | New patient, no candidate match | Intake Bot creates Patient, then back-fills `QuestionnaireResponse.subject`. | | New patient, demographic search returns candidates | Use Patient `$match` for built-in scoring; require human confirmation in a Task before linking. | | Anonymous / pre-auth start | Allow QuestionnaireResponse without `subject`; link at the next authenticated step or staff review. Track unlinked responses for cleanup. | #### Which forms and capture flows are needed? Decide whether intake uses one universal form or several, whether the same form serves both patient self-service and staff-assisted flows, and how non-form modalities (chat, voice) feed into the same data. **Questions:** - One intake form for everyone, or different forms by visit type, program, or language? - Which clinical history must be captured (allergies, medications, conditions, social determinants of health, immunizations, family history), and which answers must become distinct structured records through [extraction](#will-sdc-or-a-bot-own-extraction-and-resubmission-handling)? - Same form for patient self-service and staff-assisted, or separate forms tuned to each audience? - If you offer conversational or voice intake, does the transcript land in the same data shape as the form, or in a separate pipeline? | Situation | Approach | | :---- | :---- | | One form for everyone | Single `Questionnaire` with one authoritative extraction strategy ([Extraction](#will-sdc-or-a-bot-own-extraction-and-resubmission-handling)). | | Forms by visit type, program, or language | Multiple `Questionnaire`s with shared `linkId` conventions; Subscription criteria can target specific `questionnaire` URLs (see [Post-submission actions](#which-actions-run-after-submission-and-how-do-failures-surface)). | | Patient vs staff variants | Either reuse one Questionnaire with a "completed-by" gate, or maintain parallel Questionnaires that map to the same target resources. | | Conversational or voice intake | Keep a `Questionnaire` as the authoritative schema for what's collected; the chatbot or voice agent maps transcript fields to `linkId`s before submitting a `QuestionnaireResponse`. Downstream extraction ([Extraction](#will-sdc-or-a-bot-own-extraction-and-resubmission-handling)) stays the same. | ### Prefill and extraction #### Where does prefill come from, and who resolves conflicts? Decide whether intake starts blank or arrives pre-populated, where prefill data comes from, and how patient corrections are reconciled with the upstream source. Prefill is a superset of HIE — HIE is one source among many. **⚠ One-way door:** Once intake silently overwrites existing records from a prefill source, distinguishing "patient said X" from "source said X" after the fact requires per-field origin tracking. Set the origin-tracking pattern before launch; retrofitting historical responses is painful. **Questions:** - Does the form start blank, prefilled from data you already hold (returning patient, prior visit), or prefilled from an external source? - Which external sources are in scope (insurance card OCR, Patient Access API, Payer-to-Payer API, partner EHR, TEFCA/QHIN, state or regional HIE)? - When prefilled data and patient input disagree, who wins — the source, the patient, or staff? - Does the patient see prefilled values and explicitly confirm, or is prefill silent? | Situation | Approach | | :---- | :---- | | Source selection | Choose any combination of: internal (returning patient / prior intake), insurance card OCR, patient-mediated FHIR pull (Patient Access API / SMART-on-FHIR / partner EHR), Payer-to-Payer (CMS-0057, Jan 2027), HIE / TEFCA / QHIN. Each lands as draft resources for reconciliation. | | Conflict resolution policy | Pick one per field domain: source-wins (administrative), patient-wins (clinical history), last-write-wins, or prompt staff. Document it; don't leave it per-field. | | Provenance | Create a `Provenance` resource per prefilled field (or batched per source pull) so downstream consumers know origin and pull date. | | Patient visibility and refresh cadence | Default to "show and confirm" for clinical and demographic fields; silent prefill is acceptable for low-risk administrative fields when audit is in place. One-shot pull at form open is the safest default; re-pull on submit only for high-volatility sources (e.g. active coverage status). | #### Will SDC or a Bot own extraction and resubmission handling? Decide where extraction logic lives: on the form itself (declarative SDC rules) or in custom code – and how you handle resubmissions without creating duplicate records. **⚠ One-way door:** Once a form is in production and its field identifiers are referenced by extraction code or templates, renaming them requires migrating historical responses or maintaining a translation layer. Decide naming conventions before launch. **Questions:** - Does extraction need conditional logic (only create a subscriber/guardian record when the subscriber is not the patient, only create a consent record when the patient agreed)? - Does extraction need to call external APIs (eligibility, address validation, geocoding) during processing? - When intake is resubmitted (annual update, correction), what should happen to existing allergies, medications, conditions, and coverage records? - Will non-engineers update the form, and how often? | Situation | Approach | | :---- | :---- | | Simple field-to-resource mapping, no conditionals, no external calls | SDC `$extract` with template resources in `Questionnaire.contained`; rules ship with the form. | | Conditional resource creation, external API calls, custom validation | Bot triggered by Subscription on `QuestionnaireResponse`; use `getQuestionnaireAnswers` and `getGroupRepeatedAnswers` from `@medplum/core`. | | Mapping plus downstream actions | Choose one authoritative extractor per form: SDC or a Bot. Notifications and integrations can run afterward without independently extracting the same records. | | Retry of the same submission | Use stable source/workflow identifiers with conditional creates or updates. Reuse the same identifier for the same logical output on every retry. | | Annual update, correction, or new episode | Decide which records are amended, replaced, or newly created. Match an existing record by its established identifier or explicit reference; route ambiguous matches for reconciliation. | Patient-plus-code and beneficiary-plus-payor are not universal record identities: a patient can have distinct prescriptions, episodes, or policies with those same values. A conditional update overwrites a single match and fails on multiple matches. Define output identity and amendment behavior before choosing the write criteria. See [Working with FHIR Data](/docs/fhir-datastore/working-with-fhir). One extraction owner is an application design recommendation, not a platform prohibition on combining SDC and Bots. Different forms can use different strategies. ### Coverage and consent #### How will coverage and subscriber relationships be modeled? Decide how insurance is modeled, especially when the subscriber is not the patient, and how payers are sourced (curated directory or created on demand). **⚠ One-way door:** On the coverage record, the relationship field describes the **patient's** relationship to the subscriber; on the linked subscriber/guardian record, the relationship field describes that person's relationship to the patient – they invert (if coverage says "child", the subscriber is "parent"). Encoding this incorrectly is one of the most common intake bugs and is hard to retroactively fix on historical data. **Questions:** - Are multiple coverages (primary / secondary) captured at intake, or only primary? - Is the subscriber sometimes someone other than the patient (e.g. a parent's plan covering a child)? - Do you capture insurance card images, and where do they live? - Are payers pre-loaded as a curated directory, or created on demand? | Situation | Approach | | :---- | :---- | | Subscriber is the patient | Single `Coverage` with `subscriber` \= `Patient`; `relationship` \= `self`. | | Subscriber is not the patient | `Coverage` \+ `RelatedPerson`; remember the **inversion**: if `Coverage.relationship` is `child`, `RelatedPerson.relationship` is parent. See [Intake Data Model → RelatedPerson and Insurance Coverage](/docs/intake/intake-data-model). | | Multiple coverages | One `Coverage` per plan; use `order` to indicate primary vs secondary. | | Card image capture | Store the image as a `DocumentReference` and link it to the Coverage via `DocumentReference.context.related`; for full modeling see [Patient Insurance](/docs/billing/patient-insurance). | | Payer directory | Curated `Organization` resources searched by the form; fall back to "create if missing" only when scope is too broad to curate. | #### Which consents are required, and when must they be renewed? Decide which consent types are captured, how they're modeled, and what triggers re-consent. **Questions:** - Which consent types are required at first intake vs deferred (HIPAA, treatment, financial, telehealth, research)? - Are some consents conditional on patient attributes (minor, state of residence, payer)? - What triggers re-consent (annual update, policy change, new clinic site)? - Does intake need to capture electronic signature artifacts (image, attestation timestamp)? | Situation | Approach | | :---- | :---- | | One consent per type | One `Consent` resource per type captured (HIPAA, treatment, financial, etc.); don't bundle multiple agreements into one Consent. | | Conditional consents | Use `enableWhen` on the Questionnaire; SDC or Bot creates Consent only when the patient agreed. | | Re-consent | Existing Consent moves to `status: inactive`; new Consent created with current effective period. | | Signature artifact | Store as `Binary` referenced from `Consent.sourceAttachment`. | ### Completion and follow-up #### Does intake need orchestration and encounter linkage? Decide whether intake is a single form or a multi-step workflow with several tasks owned by different roles – and whether those tasks must show up in the provider's visit chart. *Note: The template-driven orchestration pattern is documented in detail under [Post Intake Automation → PlanDefinition Orchestration](/docs/intake/post-intake-automation). This section stays at the level needed to decide whether to adopt that pattern.* **Questions:** - Is intake completed by one person in one session, or split across roles (patient fills demographics, MA captures vitals, provider reviews)? - Does intake need to appear as tasks in the provider's visit chart? - Are some intake steps non-form work (e.g. lab orders, imaging) that should be modeled as orders rather than questionnaires? - Will the same intake template be reused across visit types or programs? | Situation | Approach | | :---- | :---- | | Single-form intake | Skip PlanDefinition; one Questionnaire plus the selected SDC or Bot extraction path is enough. | | Multi-step / multi-author intake | `PlanDefinition` with one action per Questionnaire or ServiceRequest, applied via `$apply`; the operation creates a `CarePlan`, `RequestGroup`, and one `Task` per action. | | Encounter-chart visibility | Pass `encounter` to `$apply`; each generated Task must set **both** `Task.focus` (Questionnaire/X or ServiceRequest/X – the provider UI uses this for rendering) **and** `Task.input[0].valueReference` (used to load the form). Missing either breaks the chart view. | | Reusable intake template | One `PlanDefinition` with `url`, `name`, and `title` set (the provider UI's resource search uses `name`, not `title`); referenced Questionnaires and ActivityDefinitions must also have `url` so `$apply` can resolve canonicals. | *Scheduling integration (appointments generated from intake answers, or intake gated on a scheduled visit) is covered in the [Scheduling Decision Guide](/docs/decision-guides/scheduling). From the intake side, link the resulting appointment or visit back to the intake artifacts that originated it.* #### What does intake complete mean? Decide what "intake is done" means for your operations, how that signal is represented, and what happens to incomplete intake. **Questions:** - Is "intake complete" a single moment (form submitted) or a composite state (extraction succeeded \+ eligibility verified \+ required consents on file)? - Which downstream workflows are gated on intake completion (scheduling, clinical, billing)? - For incomplete intake (patient bailed mid-form, required field missing), how is that surfaced and worked? - Are partial / draft intakes a supported state, or only completed submissions? | Situation | Approach | | :---- | :---- | | Simple completion | `QuestionnaireResponse.status` \= `completed` is the signal; nothing further to track. | | Composite completion | Single `Task` per Encounter with `businessStatus` advancing through extraction, eligibility, consent, and ready-for-clinical; or use `CarePlan.status` when intake is orchestrated via PlanDefinition ([Orchestration](#does-intake-need-orchestration-and-encounter-linkage)). | | Incomplete intake | `QuestionnaireResponse.status` \= `in-progress`; surface via a Task assigned to registration so it doesn't disappear. | | Downstream gating | Scheduling, clinical chart, and billing surfaces filter on the chosen completion signal; keep the definition single-sourced. | #### Which actions run after submission, and how do failures surface? Decide what happens after intake is submitted: which automations fire, when eligibility runs, and which downstream actions are part of intake vs separate workflows. *Note: This section is decision-focused. For the full pattern catalog, see [Post Intake Automation](/docs/intake/post-intake-automation).* **Questions:** - Does one workflow do everything (extract \+ notify \+ verify eligibility \+ create exception tasks), or do you split concerns into multiple workflows wired to different triggers? - Should intake processing re-run when the response is updated, or only on first submit? - When does eligibility verification run – during form completion, on submit, or async after intake is committed? - Which post-intake actions are required to mark intake "done" vs nice-to-have side effects? - When eligibility, address validation, or another external call fails, does that block intake or surface an exception task? | Situation | Approach | | :---- | :---- | | Monolithic intake Bot | One Bot triggered when the response first becomes `completed` handles extraction and downstream actions. Track completed steps so recovery does not repeat successful side effects. | | Split by concern | Separate Subscriptions: one on `QuestionnaireResponse` for extraction, one on `Coverage` for eligibility, one on `Patient` for welcome notifications. Each Bot has a single responsibility and audit trail. | | Responses are first saved as completed | A create-only Subscription with `status=completed` can handle first submission if drafts are never persisted. | | Drafts are saved before submission | Subscribe to create and update, filter for `status=completed`, and use transition criteria for the first entry into completed. A create-only trigger misses draft completion. | | Completed responses can be amended | Define a separate amendment/reconciliation path. A first-completion trigger intentionally skips later completed-to-completed updates. | | Exception handling | When required data is missing or an external call fails, create a `Task` for staff (`status: requested`, `owner` \= registration pool); do not silently drop the response. | | Eligibility — sync gate (during form or on submit) | `CoverageEligibilityRequest` results gate "intake complete" ([Completion](#what-does-intake-complete-mean)). Highest UX cost, lowest downstream rework. Use when day-one billing readiness matters. | | Eligibility — async post-intake | Same Subscription pattern, but failures surface as exception Tasks rather than blocking intake. Use when intake throughput trumps day-one billing readiness. | | Care-team / program routing | Bot creates Tasks or CareTeam memberships based on intake answers (e.g. screening result triggers a behavioral health referral). | For first completion, use the [FHIRPath criteria extension](/docs/subscriptions/subscription-extensions) with `%current.status = 'completed' and (%previous.status.empty() or %previous.status != 'completed')`, alongside completed search criteria and create/update events. Test initial completed creation, draft completion, and later edits. Keep processing idempotent and apply the [subscription scope and recovery guidance](/docs/subscriptions#planning-automations). --- ## Messaging & Communications Decision Guide Use this guide to design communication among patients, care teams, and external participants. Work through thread structure, routing, read state, attachments, automation, and delivery channels to choose the FHIR model and responsibilities for each messaging workflow. _Companion to the [Messaging & Communications](/docs/communications) docs._ Start with the conversation's purpose and participants: 1. **[Define conversations and content](#conversations-and-content).** Choose thread participants and patient context, then add attachments, editing, or drafts where needed. 2. **[Assign response ownership](#response-ownership)** when messages create work for a team. Define assignment, reassignment, escalation, and any automated follow-up. 3. **[Choose delivery and read state](#delivery-and-read-state).** Decide whether polling or live updates fit the workflow, what read state is needed, and which external channels support delivery or replies. 4. **[Decide when messaging is a clinical encounter](#when-is-messaging-a-clinical-encounter)** if asynchronous care or billing is in scope. Use the [questions below](#before-you-start) to establish your use case, then follow the relevant sections. Add requirements beyond these examples where needed. ### Before you start #### Who is messaging whom? - Patient ↔ provider - Provider ↔ provider - Care team internally (no patient in the thread) - System/automated → patient (one-way notifications) - Some combination of the above *Why it matters: different participant types affect access control, thread structure, and whether a patient subject is always required on a thread.* #### What is the primary purpose of messaging in their product? - Clinical care coordination (e.g. async visits, follow-ups) - Patient engagement / support - Internal operational communication - Outbound notifications only (reminders, results) *Why it matters: determines how much of the feature set is actually needed and whether async encounters/billing will come up.* #### Is this a new build or replacing something they have today? - If replacing: what are they replacing, and what are the gaps that drove this? - If new: is messaging central to the product or additive? *Why it matters: existing systems often carry assumptions about data model or UX that will surface as requirements. Knowing this early prevents surprises.* ### Conversations and content #### How will threads represent participants and patient context? **Questions:** - Are threads always tied to a specific patient, or do you need internal/admin threads with no patient context? - Are threads 1:1 only, or do you need group conversations with more than two participants? - Do threads need to be tagged or categorized (e.g. by specialty, product line, urgency)? If so, do tags need to be combinable (e.g. a thread tagged both "cardiology" and "urgent")? **What the answers drive:** | Situation | Approach | | :---- | :---- | | Threads always tied to a patient | Set `subject` on the thread header and each child message; each resource needs its own patient context for patient-compartment access | | Internal/admin threads needed | `subject` is optional — don't assume it's always present in queries or UI | | 1:1 only | Include both participants, including the creator, in the header's `recipient`; simpler read receipts (see [Read state](#what-read-state-must-be-visible-and-queryable)) | | Group threads needed | Recipient list on thread header must include all participants including the thread creator | | Tags/categories needed, single dimension | Use a single `category` entry on both the thread header and child messages | | Tags/categories needed, multiple combinable dimensions | Use multiple `category` entries (e.g. one for specialty, one for credential level) — more maintainable but queries may need to match several categories | Follow the [thread data model](/docs/communications/messaging-data-model): the header has no `payload` or `partOf`; child messages carry the content and link to the header through `partOf`. The header's `recipient` is the full participant list; a child's recipients may exclude its sender. A child does not inherit patient access through `partOf`. Enforce visibility with AccessPolicy on the resources themselves. #### How will attachments be stored and used? **Questions:** - What file types need to be supported? - Do attachments need to be searchable or referenceable as clinical documents — e.g. queryable by patient or document type — or are they just files to download within a message? **What the answers drive:** | Situation | Approach | | :---- | :---- | | Files are just downloads within a message | Upload with `createAttachment` or `createBinary`, then put a `Binary/{id}` URL in the payload's `contentAttachment`. Set an appropriate Binary `securityContext`; do not embed file bytes in `Attachment.data` | | Attachments need to be clinical documents (searchable, referenceable by patient/type) | Create a `DocumentReference` and attach via `contentReference` on the payload — file becomes a first-class searchable clinical document. Note: this is the default behavior of Medplum's React `ThreadChat` component. | | Attachments are references to existing clinical resources (e.g. a lab result) | Use `contentReference` pointing at the relevant FHIR resource (e.g. `DiagnosticReport`) | Give patient documents their own patient context and govern Binary access through [securityContext](/docs/access/binary-security-context). Changing the header's participants does not automatically change permissions on earlier messages, DocumentReferences, or files. Include those resources in membership-change and revocation checks. #### Can messages be edited, and where do drafts persist? **Questions:** - Can sent messages be corrected or retracted after sending? - If so, does the original message content need to be discoverable via search (e.g. "show all edited messages"), or is it sufficient for the history to exist but not be directly queryable? - Do users need to save a draft and return to it across sessions or devices, or is per-device/browser draft storage acceptable? **What the answers drive:** | Situation | Approach | | :---- | :---- | | Sent messages cannot be edited | No special handling needed | | Sent messages can be corrected; edits need to be searchable | Retract-and-correct: mark original `status = entered-in-error`, create new message with corrected content linked via `inResponseTo` and tagged with a `correction` category. Corrections are discoverable via status and category filters. | | Sent messages can be corrected; searchability of edits not required | In-place payload update via `patchResource`. Full version history still exists via `_history` but is not searchable. | | Drafts only needed per browser/device | Store in browser `localStorage` keyed by thread ID — no server resources needed, but lost if user clears storage or switches devices | | Drafts need to persist across devices/sessions | Store as `Communication` with `status = preparation`; update as user types (debounced); promote to `in-progress` with a `sent` timestamp when sent. Filter drafts out of thread message queries with `status:not=preparation`. | ### Response ownership #### How will responses be assigned, reassigned, and escalated? **Questions:** - Do messages get assigned to specific providers, to a pool/queue by role, or both? - Can threads be reassigned or escalated after initial assignment? - After reassignment, does the previous owner need to retain visibility of the thread? - Do you need a structured audit trail of who owned a thread and why it was rerouted? **What the answers drive:** | Situation | Approach | | :---- | :---- | | Individual assignment only | Set `Task.owner` to a specific provider; retain `performerType` if a role requirement remains relevant | | Pool/queue routing | Set `Task.performerType` by role type; clear `Task.owner`; providers claim from pool | | Both | Use `performerType` for pool eligibility; set `owner` when claimed, retaining applicable role requirements | | Reassign the same response obligation | Update the existing Task's owner with an `If-Match` version check. Add a new conversation participant through the membership workflow while preserving existing participants | | Previous owner's visibility changes | Decide access through AccessPolicy and the membership workflow; review existing child messages and files as well as the header. Do not duplicate the Task just to preserve visibility | | New, independently completed obligation | Create another Task with its own owner and completion criteria; both Tasks may focus on the same thread | | Audit trail needed (free text) | Append to `Task.note` on each reroute with author and timestamp | | Audit trail needed (structured/queryable) | Create a `Provenance` resource on each reroute with a coded reason — queryable via `Provenance?target=Task/{id}` | Task ownership, conversation membership, and authorization are separate decisions. The application checks assignment eligibility and performs the updates; changing a Task does not automatically synchronize the thread. Use a transaction with version checks when the Task and header must change together. See [Message Response Tracking and Routing](/docs/communications/message-response-tracking-and-routing). #### Which events or deadlines should trigger automation? **Questions:** - Are there events that should trigger automated messages (e.g. new lab result, appointment reminder)? - Are there SLA or response-time requirements (e.g. all messages responded to within 4 hours)? - Do providers have out-of-office or availability states that should trigger automatic replies or affect routing? - Do you need to report on SLA compliance or response time metrics? **What the answers drive:** | Situation | Approach | | :---- | :---- | | Event-triggered automation (e.g. send message when lab result arrives) | Bot triggered by a Subscription on the relevant resource type | | Recurring automation (e.g. flag threads with no response after N days) | Cron-scheduled Bot that scans for stale threads and creates reminder Tasks | | Out-of-office / availability-based rerouting | Define messaging coverage or out-of-office rules; a Bot updates the existing Task when rerouting is required. If booking availability is part of the rule, calculate it from Schedules and busy/blocked Slots rather than looking for pre-created open Slots | | SLA reporting needed | Task data (`authoredOn`, status change timestamps, `owner`, `priority`) should be exported to an analytics platform — FHIR search is not designed for aggregate SLA metrics | Plan persisted subscriptions separately from live WebSocket listeners. Verify tenant scope and define idempotency, failure detection, and recovery using [Planning Automations](/docs/subscriptions#planning-automations). ### Delivery and read state #### Are live updates required, or is polling sufficient? **Questions:** - Do new messages need to appear in real time without a page refresh? - Is this needed for the patient-facing app, the provider-facing app, or both? **What the answers drive:** | Situation | Approach | | :---- | :---- | | Real-time updates needed | WebSocket subscriptions on the thread — requires this to be scoped and enabled for the project | | Polling is acceptable | Simpler implementation; periodic re-fetch of thread messages | | Notification only (e.g. badge, push) without live message rendering | Can use subscription to trigger a notification without requiring in-place message updates | #### What read state must be visible and queryable? **Questions:** - Are threads ever group conversations (more than 2 participants)? - Do you need to query unread counts via API — e.g. a badge count or a list of all unread threads for a user? - Do unread counts need to be tracked per-recipient in a group thread? **What the answers drive:** | Situation | Approach | | :---- | :---- | | 1:1 threads only | Option A: mark message `status = completed` when read; query unread via status filter. Simplest model. | | Group threads, no API unread query needed | Option B: store per-participant read state as an extension on the thread header. Shows unread dot in UI; not searchable via API. | | Group threads, API unread query needed | Option C: create a read-receipt Task per recipient per message; complete it when read. Supports badge counts and unread queries. Most complex. | #### Which external channels need outbound delivery or replies? **Questions:** - Which channels — SMS, email, other? - Is this outbound-only (notify the patient) or does the patient reply back into the thread? - If inbound replies are needed, how is the patient identified from an incoming message (e.g. phone number, email address)? - Does the external provider (e.g. Twilio) have its own conversation ID that can be stored for thread matching? **What the answers drive:** | Situation | Approach | | :---- | :---- | | Outbound notifications only | App creates the Communication, then calls `executeBot` directly to send via the external channel — not Subscription-triggered, so errors surface synchronously | | Inbound replies needed | Inbound webhook Bot receives the payload, identifies the patient, matches or creates a thread, creates a child Communication | | External conversation ID available (e.g. Twilio conversation SID) | Upsert thread header by that identifier on first message; use it as conditional `partOf` reference on all inbound messages — most reliable thread matching strategy | | No external conversation ID | Match thread by patient phone/email and open thread status; ambiguous cases need a defined fallback strategy | | Round-trip (provider replies in-app, patient receives via SMS/email) | Store channel on thread header as `medium`; app creates the Communication then calls `executeBot` to dispatch outbound — Bot reads `medium` to route to the right channel | | Webhook retry / duplicate messages likely | Use `createResourceIfNoneExist` with the provider's message ID as an identifier to prevent duplicate inbound Communications | ### When is messaging a clinical encounter? **Questions:** - Does the customer intend to bill for messaging-based care? - How do you define a "session" — one thread per session, grouped by day, or patient-initiated? - Can a single messaging session involve multiple patients (e.g. a parent asking about two children)? - Do you need to capture diagnosis codes, service type, or other clinical details per encounter? **What the answers drive:** | Situation | Approach | | :---- | :---- | | No billing intent | No Encounter resource needed; thread stands alone | | Billing needed, one patient per session | Create an Encounter per session; link to the thread header via `Communication.encounter` | | One thread \= one session | Straightforward 1:1 mapping of thread to Encounter | | Rolling interaction model (e.g. continuous text thread, grouped by day) | One Encounter spans multiple threads or a time window; multiple thread headers link to the same Encounter | | Multiple patients can be in one session | Create a parent session Encounter \+ one child Encounter per patient via `Encounter.partOf`; clinical details live on child Encounters | | Clinical documentation needed for billing | Populate `Encounter.participant`, `Encounter.reasonCode`, and `Encounter.serviceType` per patient Encounter | ### Related Integrations The external messaging channels Medplum integrates with first-party — both deliver through FHIR `Communication` resources, so they slot into the thread model above: - **[Twilio SMS](/docs/integration/twilio-sms)** — send and receive SMS via the `$send-sms-twilio` operation. - **[eFax](/docs/integration/efax)** — send and receive faxes via the `$send-efax` and `$receive-efax` operations. --- ## RCM & Billing Decision Guide Use this guide to decide which parts of the revenue cycle your application owns and which it delegates to billing partners. Work through coverage, charge capture, claims, remittance, and patient responsibility to define the data model, integrations, and operational handoffs for your reimbursement model. _Companion to the [Billing](/docs/billing) docs._ Start with reimbursement and ownership, then follow the relevant financial workflows: 1. **Choose your [reimbursement model](#what-is-your-reimbursement-model) and [workflow ownership](#how-much-of-the-revenue-cycle-runs-in-medplum-versus-delegated).** Decide what your application owns and what a billing partner or existing system handles. 2. **Define [coverage and accounts](#coverage-and-accounts) and [checks before service](#checks-before-service).** Establish responsibilities for balances, eligibility, authorization, and estimates where applicable. 3. **[Define charge capture and claim submission](#charges-and-claims).** Decide how services are coded and priced; include claims when billing payers or when a partner requires that submission format. 4. **[Assign payer-response and accounts-receivable responsibilities](#payer-responses-and-accounts-receivable).** Define posting, denials, follow-up, and reporting, including the boundary with any partner that owns them. 5. **[Define patient billing](#how-will-patients-be-billed)** if patients owe balances or pay directly. Decide who collects payments and provides statements or superbills. 6. **[Plan value-based contracts](#how-will-value-based-contracts-work)** if you operate under capitation or risk-based arrangements. Use the [questions below](#before-you-start) to establish your use case, then follow the relevant sections. Add requirements beyond these examples where needed. ### Before you start #### What is your reimbursement model? - Insurance / claims-based – you bill payers and reconcile their responses - Patient self-pay / cash – patients pay you directly; no payer claim - Value-based / capitation – per-member-per-month or risk-based - Some combination *Why: Claims-based workflows cover every applicable decision; direct self-pay uses charge capture and patient billing. Partner-managed self-pay may also require claim assembly as the partner's submission format ([Patient billing](#how-will-patients-be-billed)). Value-based workflows start with the coverage and account model, then the value-based decision ([Value-based contracts](#how-will-value-based-contracts-work)).* #### How much of the revenue cycle runs in Medplum versus delegated? - **Clearinghouse-direct** – Medplum runs the cycle; the clearinghouse is transport. You own scrubbing, denials, and remittance posting. The most complete documented path. - **RCM-automation partner** – you send encounter and charge data and delegate the back end; agree which scrubbing, payer routing, denials, resubmission, patient collections, and reporting workflows the partner owns. Define the submission interface and which billing tasks or status changes sync back into Medplum. - **Documents only (no electronic claims)** – you don't run claims through a clearinghouse or RCM partner. Generate a CMS-1500 PDF for the occasional paper/portal payer, or hand patients a superbill to self-submit for out-of-network reimbursement. Common for cash-pay and OON practices (see [Reimbursement model](#what-is-your-reimbursement-model)); also a per-payer fallback you can layer onto any engine above. - **External Practice Management (PM) / billing system** – Medplum is the clinical source of truth; charges or claims hand off to an existing PM/billing system that owns the rest. *In one line: a clearinghouse moves your claims; an RCM platform runs your revenue cycle. Either way Medplum owns the front half (coverage, charge capture, claim assembly); the paths diverge on the back half.* | Revenue-cycle stage | Clearinghouse | RCM partner | Documents only | External PM | | :---- | :---- | :---- | :---- | :---- | | [Coverage](#how-will-coverage-subscribers-and-payers-be-represented) & [charge capture](#what-creates-a-charge-and-how-is-it-coded-and-priced) | You build | You build | You build | You build | | [Eligibility](#what-eligibility-and-benefit-evidence-is-required-and-when) | Eligibility integration; you own timing and follow-up | Partner workflow or separate eligibility integration | Manual or separate eligibility integration | External | | [Claim assembly](#how-will-claims-be-assembled-validated-and-submitted) | You build | You build | You build | External | | Scrubbing / validation | You build | Partner owns | Light | External | | [Submission](#how-will-claims-be-assembled-validated-and-submitted) | You build (837P) | Partner submit op | PDF / patient files | External | | [Responses & ERA posting](#how-will-payer-responses-be-matched-and-remittances-posted) | You own report ingestion, interpretation, and posting; verify what the integration supplies | Partner owns; define status/task sync | Manual | External | | [Denials & resubmission](#who-works-rejections-denials-resubmissions-and-appeals) | You build | Partner owns; define status/task sync | Manual | External | | [AR & reporting](#how-will-staff-track-balances-aging-and-unresolved-work) | You build | Partner owns | Manual | External | **How to choose:** | | Clearinghouse | RCM partner | | :---- | :---- | :---- | | You get | Full control; billing data stays as FHIR in Medplum | A payer-rules engine and in-house billers; far less to build | | Trade-off | You build and maintain the entire back office | Recurring fees; less control; AR and reporting live on the partner's platform | | Choose if | You have RCM expertise and want a fully owned cycle | You want to outsource the cycle | *Paths can mix. The choice sets which workflows you build ([Charge capture](#what-creates-a-charge-and-how-is-it-coded-and-priced), [Claim submission](#how-will-claims-be-assembled-validated-and-submitted), [Responses and posting](#how-will-payer-responses-be-matched-and-remittances-posted), [Denials and appeals](#who-works-rejections-denials-resubmissions-and-appeals), [Patient billing](#how-will-patients-be-billed), [AR and reporting](#how-will-staff-track-balances-aging-and-unresolved-work)) vs. read only for the boundary.* #### Who does the billing work, and is the patient part of it? - Charge capture: clinicians at point of care, or back-office entry? - Coding and claim work: dedicated coders/billers, or automated with exception review? - Accounts receivable (AR) follow-up: who works denials, aging, and patient balances? - Patient: do patients see estimates, statements, or pay online? *Why: drives charge-capture design ([Charge capture](#what-creates-a-charge-and-how-is-it-coded-and-priced)), who works denials and AR ([Denials and appeals](#who-works-rejections-denials-resubmissions-and-appeals), [AR and reporting](#how-will-staff-track-balances-aging-and-unresolved-work)), and whether patient-facing billing is in scope ([Patient billing](#how-will-patients-be-billed)).* #### Where does coding happen, and how automated is it? - Auto-derived from clinical data (visit type, orders, procedures)? - Coder-reviewed before the claim is built? - Fully manual? *Why: drives the coding and review model in charge capture ([Charge capture](#what-creates-a-charge-and-how-is-it-coded-and-priced)) and where scrubbing exceptions surface ([Claim submission](#how-will-claims-be-assembled-validated-and-submitted)).* #### New build or replacing/extending, and what else must billing touch? - If replacing: what billing/PM system, and what gaps drove the change? - If new: is billing core to the product (you're building an RCM/billing product) or additive (a clinical app that also needs to bill)? - What other systems must billing read from or write to (clinical/EHR source, PM, clearinghouse, accounting/GL, payment processor, payer portals)? *Why: surfaces integration boundaries and the source-of-truth split behind [Workflow ownership](#how-much-of-the-revenue-cycle-runs-in-medplum-versus-delegated), [Claim submission](#how-will-claims-be-assembled-validated-and-submitted), and [Responses and posting](#how-will-payer-responses-be-matched-and-remittances-posted). Core-to-product builds tend to own the cycle (a clearinghouse); additive builds tend to delegate (an RCM partner or external PM).* ### Coverage and accounts #### How will coverage, subscribers, and payers be represented? How you represent a patient's insurance (the coverage stack, the subscriber, payers, cost-sharing) is the foundation every claim and eligibility check builds on. See [Representing Patient Insurance Coverage](/docs/billing/patient-insurance); coverage capture at intake is in the [Intake guide](/docs/decision-guides/intake#how-will-coverage-and-subscriber-relationships-be-modeled). **Questions:** - Do patients commonly have more than one coverage, and must you bill them in order? - Is the subscriber sometimes someone other than the patient? - Is your payer list curated in Medplum, synced, or created on demand? - What cost-sharing (copay, coinsurance, deductible) do you need for estimates and patient billing? | Situation | Approach | | :---- | :---- | | Subscriber is the patient | One `Coverage`; `subscriber` \= `Patient`; `relationship` \= `self`; `payor` → `Organization`. | | Primary \+ secondary | One `Coverage` per plan; `Coverage.order` sets billing sequence. Secondary submits after the primary remittance posts ([Responses and posting](#how-will-payer-responses-be-matched-and-remittances-posted)). | | Subscriber ≠ patient | `Coverage` \+ `RelatedPerson` for the subscriber. The relationship inverts: `Coverage.relationship` is the patient's relationship to the subscriber, the `RelatedPerson`'s is the reverse (coverage `child` → related person `parent`). Getting this backwards is a common billing bug. | | Member ID & plan details | `Coverage.subscriberId` for member ID; `Coverage.class` for group, plan name, RxBIN/RxPCN. | | Cost-sharing | `Coverage.costToBeneficiary`; feeds estimates ([Authorization and estimates](#how-will-prior-authorization-and-patient-estimates-be-handled)) and patient responsibility ([Patient billing](#how-will-patients-be-billed)). | | Payer directory | Medplum ships a built-in **Payer Directory** of `Organization`s, or curate your own. Verify the identifier system for each eligibility and claim-submission route; they may differ even for the same partner. | | Card images | Store as `DocumentReference`/`Binary` linked to the `Coverage`. | #### Does the application need an account and financial ledger? Decide whether you track a patient financial account that rolls charges, claims, and payments into a balance (the backbone for AR in [AR and reporting](#how-will-staff-track-balances-aging-and-unresolved-work) and patient billing in [Patient billing](#how-will-patients-be-billed)), or let each charge stand alone. FHIR R4 supplies account and billing resources, but an Account is not a computed ledger. Choose the posting model, claim-to-account association, reversals, and reconciliation rules explicitly; these are application responsibilities. **Questions:** - Do you need a single balance rolling up charges, payer payments, adjustments, and patient payments? - Is the account scoped per patient, per visit, or per episode? - Do you track payer and patient responsibility separately on the same account? - Must the account reconcile against an external accounting/GL system? | Situation | Approach | | :---- | :---- | | Account links | Use `ChargeItem.account`, `Invoice.account`, and `Encounter.account` to reference an Account. R4 Claim and PaymentReconciliation have no dedicated `account` element | | Account scope | Set `Account.subject` to the Patient. For a visit account, link it from `Encounter.account`; Account.subject does not reference Encounter | | Claims and remittances | Define an explicit claim-to-account association in your application model. `ClaimResponse.request` links to Claim; `PaymentReconciliation.detail.request` and `.response` can identify the related request and response. These links do not post money | | Running balance | Calculate charges, payments, adjustments, and reversals from uniquely identified postings. R4 Account has no `balance` field; decide where the postings and calculated totals live | | Payer vs. patient | Track responsibility allocations without counting the same charge again when creating a patient Invoice. Decide whether Medplum or the partner owns each balance | | No ledger | Skip `Account` for pure pass-through submission where another system owns the balance, but you lose in-Medplum AR ([AR and reporting](#how-will-staff-track-balances-aging-and-unresolved-work)). | For example, a visit can have `Account.subject → Patient`, `Encounter.account → Account`, and `ChargeItem.account → Account`. A patient Invoice can use `Invoice.account → Account` and `Invoice.lineItem.chargeItemReference → ChargeItem`. Associate the submitted Claim with that account separately, and record each applied remittance/payment once. The resource links support reconciliation; creating these resources alone does not calculate a balance. ### Checks before service #### What eligibility and benefit evidence is required, and when? Decide whether you verify coverage before service, what you ask the payer, and when the check runs. See [Insurance Eligibility Checks](/docs/billing/insurance-eligibility-checks). Choose whether eligibility runs through your clearinghouse, RCM partner, or a separate integration, including whether real-time X12 270/271 exchange is needed. Confirm supported service types, payer routing identifiers, and how responses map to CoverageEligibilityResponse resources. See [Related Integrations](#related-integrations) for the available first-party routes. **Questions:** - What must you confirm: active coverage, covered service, in-network status, or benefit/cost detail? - When does the check run: scheduling, check-in, or batch pre-visit? - Do you re-check on a cadence for ongoing patients? - Who works failed or ambiguous checks? | Situation | Approach | | :---- | :---- | | Verify coverage / benefits | Create a `CoverageEligibilityRequest` with `purpose: benefits` and invoke the selected eligibility operation; retain the response and source evidence | | Service-specific benefits | Request supported X12 service types through `CoverageEligibilityRequest.item.category`. A payer may still return general or incomplete benefits; define the fallback when the answer is insufficient | | In-network check | Interpret the benefit-network indicator in context. It is not independent verification of the provider's contract, location, or service; ambiguous cases need payer confirmation | | Cost-sharing | Copay, coinsurance, remaining deductible, and out-of-pocket land in `CoverageEligibilityResponse.insurance.item`; the input to patient estimates ([Authorization and estimates](#how-will-prior-authorization-and-patient-estimates-be-handled)). | | When it runs | A `Bot` issues the request on the triggering event; persist the response for the visit and billing. | | Exception | On failure or ambiguity, create a `Task` for registration rather than proceeding silently. | `CoverageEligibilityResponse.outcome = complete` means processing completed without reported errors. It does not establish coverage for every service, network participation, or prior-authorization approval. Use the evidence and exception-handling approach in the [Referrals Decision Guide](/docs/decision-guides/referrals). #### How will prior authorization and patient estimates be handled? Decide whether services need payer authorization before they happen, how you track that to a decision, and whether you produce patient cost estimates up front. *Note: the eligibility- and Claim-based rows below are one FHIR approach to prior auth; validate against your payer requirements. The standards-based CMS-0057-F path (CDS Hooks / CRD) is in [alpha](/docs/integration/electronic-prior-auth). Prior auth in a referral context is in the [Referrals guide](/docs/decision-guides/referrals#who-owns-the-work-and-what-gates-authorization-and-scheduling).* **Questions:** - Do any services require prior authorization before scheduling or delivery? - How do you check whether auth is required, and track a submitted auth to approval/denial? - Must scheduling be gated until authorization clears? - Do you owe patients a cost estimate before service (e.g. a Good-Faith Estimate)? | Situation | Approach | | :---- | :---- | | Is auth required? | Collect requirements for the service, payer, provider, and date. Use `purpose: auth-requirements` only through a route that supports it; a benefits check alone may not answer the question | | Submit a prior auth | A FHIR model can use `Claim.use: preauthorization` and a corresponding ClaimResponse, but you must select a supported payer/vendor submission workflow. Professional-claim submission does not imply prior-auth transport support | | Standards-based (CMS-0057-F) | Electronic prior auth via CDS Hooks / Coverage Requirements Discovery (CRD); [alpha](/docs/integration/electronic-prior-auth). The emerging payer-interop path as the rule phases in. | | Gate scheduling | Hold work in an auth-pending `Task.businessStatus` until the required approval evidence is verified for the service, dates, and units. Processing outcome `complete` alone is not approval | **Good-Faith Estimates (No Surprises Act).** One FHIR approach: build the estimate from pricing rules (`ChargeItemDefinition`/`$apply`, [Charge capture](#what-creates-a-charge-and-how-is-it-coded-and-priced)) and the patient's cost-sharing (`Coverage.costToBeneficiary`, [Coverage](#how-will-coverage-subscribers-and-payers-be-represented)), delivered as a `DocumentReference`; validate against your compliance requirements. Mainly for self-pay and out-of-network patients. ### Charges and claims #### What creates a charge, and how is it coded and priced? *For insurance claims, cover every subsection. Direct self-pay needs pricing ([Charge capture](#what-creates-a-charge-and-how-is-it-coded-and-priced)), patient billing ([Patient billing](#how-will-patients-be-billed)), and AR ownership ([AR and reporting](#how-will-staff-track-balances-aging-and-unresolved-work)); partner-managed self-pay may also need claim assembly and submission ([Claim submission](#how-will-claims-be-assembled-validated-and-submitted), [Patient billing](#how-will-patients-be-billed)).* Decide what gets billed, where the codes come from, and how each line is priced. Coding is the largest source of denials, so treat it as first-class, not a byproduct of charge entry. **Questions:** - What generates a charge: a completed encounter, an order/procedure, a fulfilled service? - Where do codes come from (CPT/HCPCS, ICD-10, modifiers): auto-derived, coder-reviewed, or manual ([Coding ownership](#where-does-coding-happen-and-how-automated-is-it))? - Is pricing a single fee schedule, or does it vary by payer contract? - Who reviews coding before a claim is built, and how do exceptions surface? | Situation | Approach | | :---- | :---- | | What gets billed | One `ChargeItem` per billable line; `ChargeItem.code` carries CPT/HCPCS. Link `context` → `Encounter` and reference the `Account` ([Accounts](#does-the-application-need-an-account-and-financial-ledger)) at creation, or reconciling into claims and AR later gets painful. | | Pricing | `ChargeItemDefinition` holds the rules; [`$apply`](/docs/api/fhir/operations/chargeitemdefinition-apply) calculates the price. | | Payer-specific rates | Multiple `ChargeItemDefinition`s keyed to payer/contract; apply the one matching the coverage. | | Coding source | Auto-derive via a `Bot`, or set by a coder; route low-confidence items to a review `Task`. | | Diagnoses | Capture ICD-10 diagnoses to attach to the claim (`Claim.diagnosis`, with item-level pointers) in [Claim submission](#how-will-claims-be-assembled-validated-and-submitted). | #### How will claims be assembled, validated, and submitted? Decide how you build a valid claim from charges and coverage, and how it leaves your system. The workflow ownership choice in [Workflow ownership](#how-much-of-the-revenue-cycle-runs-in-medplum-versus-delegated) plays out here. **⚠ One-way door:** Choosing a submission backend is hard to reverse. Each maps the `Claim` to a different format (837P, the partner's API, a CMS-1500 PDF) with a different response pipeline ([Responses and posting](#how-will-payer-responses-be-matched-and-remittances-posted)), and payer identifier systems differ. Keep one `Claim` as the source of truth so the backend stays swappable. **Questions:** - Are you billing as an organization or an individual provider? - Who rendered the service, and which service facility should appear on the submission? - Which submission path(s), and does it vary by payer (electronic for most, paper/portal for some)? - What validation must pass before submission, and where do failures surface? - Do you submit secondary claims after the primary pays (coordination of benefits)? | Situation | Approach | | :---- | :---- | | Build the claim | `Claim` referencing the `Coverage` (`Claim.insurance`), charges (`Claim.item`), and ICD-10 diagnoses (`Claim.diagnosis`) with item-level pointers. | | Billing provider | Set on `Claim.provider`: the billing `Organization` (or a `Practitioner` for individual billing). The rendering provider goes on `Claim.careTeam` with role `primary`. | | Service facility | Set `Claim.facility` to the service Location when required. Confirm the submission route's facility requirements and fallback behavior, including individual billing and telehealth | | Electronic (you own the cycle) | The clearinghouse submit operation sends an 837P and writes a correlation id back to the `Claim`. | | RCM partner | Submit through the partner's supported interface; persist its acknowledgment and correlation identifiers. Agree which downstream workflows it owns and how billing tasks or status changes sync back ([Denials and appeals](#who-works-rejections-denials-resubmissions-and-appeals)). | | Manual / paper / portal | [`Claim/$export`](/docs/api/fhir/operations/claim-export) generates a [CMS-1500 PDF](/docs/billing/creating-cms1500) (`Binary`; experimental); log the outbound document. | | Patient-submitted (out-of-network) | Generate a [superbill](/docs/billing/creating-superbills) for the patient to file. | | Validation and submission failure | Validate before submit and surface failures in a Task. Use the returned ClaimResponse outcome/errors when available and handle thrown invocation errors separately. `error` is not a valid R4 Claim.status; keep submission state distinct from the Claim lifecycle | | Multiple channels | One `Claim` is the source of truth; run separate send paths per payer without diverging content. | ### Payer responses and accounts receivable #### How will payer responses be matched and remittances posted? Decide how you ingest what the payer sends back (the *277CA* acknowledgment, accepted or rejected pre-adjudication, and the *835 ERA* remittance, what was paid, adjusted, or denied line by line) and how payments post. These share one pipeline, so design them together. *On the clearinghouse path, distinguish report ingestion from adjudication interpretation and posting. Verify which steps the integration implements and build the rest. With an RCM partner, confirm its posting responsibilities and the status or task updates available to your application.* **Questions:** - How do responses arrive: webhook push, polling, or partner API? - How do you match a response to the originating claim? - How do payer payments and adjustments post to the account? - What share posts automatically vs. needs review? | Situation | Approach | | :---- | :---- | | Ingestion | Define webhook delivery, polling, and recovery of missed reports. Deduplicate by the source transaction identifier; make interpretation and posting independently idempotent | | Acknowledgment (277CA) | Retain the raw acknowledgment, for example as a DocumentReference, and correlate it with the originating submission. Distinguish acceptance or rejection from adjudication | | Remittance (835 ERA) | Retain source reports and map payment, adjustment, and responsibility details into your posting model. Storing a report alone does not create PaymentReconciliation resources or post money | | Matching | Use the submission's patient-control-number and correlation identifiers. Provide an unmatched-report queue rather than assuming every report can be linked automatically | | Source claim status | Define where external status is stored and how it advances your workflow. Keep submission outcome, payer status, adjudication amounts, and payment posting distinct | | Adjudication and posting | Build the report-to-ledger mapping, including line amounts, responsibility, adjustments, and reversals. If using PaymentReconciliation or additional ClaimResponses, define their role and correlation explicitly | | Review | Auto-post only after the application's validation and reconciliation rules pass; route mismatches and ambiguous reports to a Task | #### Who works rejections, denials, resubmissions, and appeals? Decide how you work claims the payer rejects or denies: correcting and resubmitting, or appealing. *Note: The rows below are one FHIR approach to denial handling; validate against your workflow. On the clearinghouse path you build this in Medplum; with an RCM partner, confirm how its denial work and status changes surface back in your application.* **Questions:** - Do you work front-end rejections (277CA) differently from adjudicated denials (835)? - When you fix and resend, is it a corrected claim or a fresh submission? - Do you track appeals (deadlines, supporting documentation, outcome)? - How do staff find claims that need work? | Situation | Approach | | :---- | :---- | | Surface denials | Interpret rejection or denial detail from the submission response and stored payer reports ([Responses and posting](#how-will-payer-responses-be-matched-and-remittances-posted)); create a Task work queue for the biller | | Corrected claim | Resubmit a corrected `Claim` linked to the original (`Claim.related`), via the same backend ([Claim submission](#how-will-claims-be-assembled-validated-and-submitted)). | | Retry a rejected submission | Check the route's retry and duplicate-submission behavior. Preserve prior responses and submission history; do not delete active ClaimResponses simply to bypass a guard. An accepted submission needs a deliberate correction workflow | | Appeals | Track as a `Task` with deadlines; attach supporting docs as `DocumentReference`. | | Find work | Search over open denial `Task`s ([AR and reporting](#how-will-staff-track-balances-aging-and-unresolved-work)). | | Partner-owned | Agree who works denials and resubmission, which exceptions return to your team, and how status or task updates enter Medplum. | #### How will staff track balances, aging, and unresolved work? Decide how you track where every claim and balance sits, and how staff find work that's stuck or overdue. *Note: The rows below are one FHIR approach to AR tracking; validate against your reporting needs. Most relevant on the clearinghouse path; an RCM partner owns AR and reporting.* **Questions:** - What lifecycle states matter (e.g. draft → submitted → accepted → paid → denied → closed)? - How do you standardize states so queues, reports, and integrations agree? - Do you track aging (days since submission, days pending) and AR buckets? - How do staff find overdue or stuck claims and balances? | Situation | Approach | | :---- | :---- | | Lifecycle states | Track progress with `Task.businessStatus`; move `Task.status` on clear transitions. Keep the state set small and single-sourced. | | External signals | Bots interpret submission responses and payer reports, then advance the appropriate workflow stage. Keep accepted, adjudicated, and posted states distinct | | Aging & AR | Record submission and stage-entry times, derive stage intervals from history or explicit transition records, and age calculated outstanding balances. `Claim.created` is not submission time; `Task.lastModified` may reflect unrelated edits | | Overdue work | Scheduled `Bot`s or subscriptions flag items past an SLA into work queues. | ### How will patients be billed? Decide how you bill patients for their share (copays, coinsurance, deductibles, full self-pay) and how they pay. **Questions:** - What balances do you bill: post-adjudication responsibility, self-pay, or both? - Will you collect self-pay directly or delegate it to a billing partner? - Do you issue statements, and on what cadence? - Do you take online payment / card on file, through which processor? - For out-of-network patients, do you provide a superbill instead of billing the payer? | Situation | Approach | | :---- | :---- | | Patient balance | `Invoice` for patient-owed amounts rolling to the `Account` ([Accounts](#does-the-application-need-an-account-and-financial-ledger)); responsibility from remittance ([Responses and posting](#how-will-payer-responses-be-matched-and-remittances-posted)) or self-pay pricing ([Charge capture](#what-creates-a-charge-and-how-is-it-coded-and-priced)). | | Online payment | Integrate a payment processor; a [sample integration](/docs/billing#sample-integrations) shows keeping invoices and payments in sync. | | Direct self-pay / cash | Price via `ChargeItemDefinition`/`$apply` ([Charge capture](#what-creates-a-charge-and-how-is-it-coded-and-priced)) and invoice the patient directly; no payer claim | | Partner-managed self-pay | Confirm the partner's encounter/charge submission format and how it represents self-pay responsibility. If that format uses a FHIR Claim, distinguish this data handoff from an insurance claim sent to a payer | | Out-of-network | Provide a superbill ([Claim submission](#how-will-claims-be-assembled-validated-and-submitted)) for the patient to submit for reimbursement. | ### How will value-based contracts work? Value-based care isn't a claims pipeline; it splits into measurement (quality, gaps in care) and money (capitation, risk, settlement). Read only for capitated or risk-based contracts. *Note: Quality measurement uses documented operations; the financial rows are one FHIR approach to validate against your contracts. Quality/HEDIS reporting overlaps clinical analytics; see [Analytics](/docs/analytics).* **Questions:** - Which contracts are capitated or risk-based, and which members are attributed to you? - Which quality measures (eCQM, HEDIS, stars) are you accountable for? - Do you still submit claims or encounter data when capitated (for risk adjustment and measurement)? - How are capitation payments and any shared-savings settlement reconciled? | Situation | Approach | | :---- | :---- | | Attribution / panel | `Group` as the member roster; ingest the payer roster via a `Bot`; `CareTeam` for accountable teams. | | Quality measurement & gaps in care | `Measure` \+ `MeasureReport` via [`Measure/$evaluate-measure`](/docs/api/fhir/operations/evaluate-measure); eCQM and HEDIS via [Analytics](/docs/analytics). | | Risk adjustment (HCC / RAF) | Capture `Condition` coding from encounters and submit as encounter data; ties to coding ([Charge capture](#what-creates-a-charge-and-how-is-it-coded-and-priced)). | | Encounter data | Even when capitated, reuse claim assembly ([Claim submission](#how-will-claims-be-assembled-validated-and-submitted)) to send 837/encounter data for measurement and risk. | | Payments (PMPM, settlement) | If using PaymentReconciliation, define its association to contract, period, and account in the posting model ([Accounts](#does-the-application-need-an-account-and-financial-ledger)). Receiving a reconciliation resource does not automatically change an Account balance | ### Related Integrations See the integration docs for setup and implementation details: - [Stedi](/docs/integration/stedi): clearinghouse connectivity for eligibility, professional claims, and claim responses. - [Candid Health](/docs/integration/candid): RCM partner integration for claim submission and billing-task synchronization. --- ## Referrals Decision Guide A referral needs more than a send button. Use this guide with your clinical and operations teams to decide who owns each step, what the receiving team needs, and how you will know the loop is closed. _Companion to the [Referrals](/docs/careplans/referrals) docs._ Define the shared referral model, then follow the sending or receiving path: 1. **[Define what a referral represents](#what-does-a-referral-represent).** Choose service categories, urgency, identifiers, and draft or release semantics. 2. **[Plan preparation and delivery](#preparing-and-sending-referrals)** if you originate referrals. Define capture, recipients, clinical information, and delivery channels. 3. **[Plan acceptance of incoming referrals](#how-will-incoming-referrals-be-accepted)** if you receive them. Define intake channels, patient matching, review, and duplicate handling. 4. **[Assign coordination and closure](#coordination-and-closure).** For every workflow, define ownership, authorization and scheduling gates, lifecycle stages, expected results, and what closes the loop. Use the [questions below](#before-you-start) to establish your use case, then follow the relevant sections. Add requirements beyond these examples where needed. ### Before you start #### Which side(s) of the referral lifecycle do you build for? - Outbound only – you originate referrals to others - Inbound only – you receive referrals from others - Both *Why: determines which decisions apply.* #### Who interacts with referrals, and is the patient part of the workflow? - Sending side: which roles author, edit, and send referrals (clinicians, MAs, coordinators, intake staff)? - Receiving side: which roles triage, accept, and work referrals on intake? - Patient: do patients see referrals, get notified, or take action (e.g. view in portal, self-schedule, decline)? *Why: drives how forms are designed ([Capture](#which-capture-flows-and-originating-context-are-required)), who notes are written for ([Clinical information](#what-clinical-information-and-notes-will-travel-with-the-referral)), who owns the work ([Ownership and scheduling](#who-owns-the-work-and-what-gates-authorization-and-scheduling)), and whether patient-facing content is in scope.* #### Where do receiving providers live relative to your tenant? - Inside your tenant only - Partner tenants or organizations - External organizations without FHIR connectivity - Some combination *Why: drives how recipients are stored and selected ([Recipients](#how-will-recipients-be-selected-and-maintained)), and which outbound channels you support ([Delivery](#which-channels-and-payloads-will-deliver-the-referral)).* For teams sharing one project, create receiving work against the shared ServiceRequest directly. Add transmission and identifier reconciliation when records cross systems. See [internal and external referrals](/docs/careplans/referrals#decide-whether-the-teams-share-a-project). #### Is this a new build or replacing/extending something, and what other systems must it work with? - If replacing: what system or workflow, and what gaps drove the change? - If new: is referrals central to the product or additive? - What other systems will referrals flow to or from (source EHR, downstream EHRs at receivers, billing, HIE, payer portals, include DirectMessage)? *Why: surfaces migration needs and integration boundaries that affect outbound channels ([Delivery](#which-channels-and-payloads-will-deliver-the-referral)), inbound intake ([Inbound referrals](#how-will-incoming-referrals-be-accepted)), and matching of patient and provider records across systems.* ### What does a referral represent? Each independently managed requested service uses a `ServiceRequest`; requests authorized together can share a `requisition` identifier. Decide how you classify it, how urgency is expressed, whether you expose a staff-facing reference number, and whether drafts exist before send. **Questions:** - How do you classify referrals (specialty, service, program), using standard codes, your own list, or both? - What urgency options do users pick, and should they match triage and reporting? - Do you show a referral number or reference beyond the system id? - Is the referral final at send/commit, or do you need a draft first? | Situation | Approach | | :---- | :---- | | Category and service type | Bind `category` / `code` to agreed ValueSets (SNOMED, custom, or mix); drives search and reporting. | | Priority | Set `ServiceRequest.priority`; keep labels aligned with clinical operations. | | Display referral number | `ServiceRequest.identifier` with your namespace; assign at create when needed for ops or correspondence. | | Released order | Set `intent = order` and `status = active` through the authorized release path. Sending is tracked separately from order status. | | Draft before release | Use `status = draft` with `intent = order` for an order being prepared. For a true proposal, retain it and create a separate authorized order with `basedOn` referencing the proposal. | ### Preparing and sending referrals #### Which capture flows and originating context are required? Decide how referral intake is structured in the UI – one flow vs several by specialty or program, how answers are constrained, and whether every referral must link to an originating visit. **Questions:** - Do you use one referral intake flow for everyone, or different flows by specialty, service, or program? - Which fields must be chosen from lists or structured inputs (e.g. specialty, recipient, urgency), which allow free text, and what is required before the referral can be sent? - Must every referral tie to the visit where it was created, or can referrals start without a linked visit (e.g. intake, triage, outreach)? | Situation | Approach | | :---- | :---- | | One form | Use one Questionnaire and retain its QuestionnaireResponse. Choose one extraction strategy for the form: SDC template extraction or a Bot mapping to ServiceRequest. | | Specialty forms (e.g., variation based on specialty, service, etc.) or supplemental forms (specific extra information needed based on receiving clinic preferences) | Multiple Questionnaires; shared core fields \+ specialty-specific items (specialty forms) or additional Questionnaires specific to receiving clinics (supplemental forms); map each to `ServiceRequest`. | | Encounter required | Require `encounter` when referrals always start from a scheduled or in-progress visit. | | Encounter optional | Allow missing `encounter` when referrals can start outside a visit (e.g. intake); enforce `subject` and clinical reason instead. | #### How will recipients be selected and maintained? Decide who users may send a referral *to* (person vs site vs queue) and where your recipient directory lives – maintained in Medplum, fed from another system, or both. **⚠ One-way door:** If recipients start as free text and you move to a structured directory later, expect to reconcile historical referrals against the new directory. **Questions:** - Can a referral go to a specific clinician, to a site, department, or queue, or either, depending on referral type? - Is your recipient directory edited and stored in Medplum, synced from another system, or mixed? - Should the recipient picker show or filter by in-network status for the patient's coverage – and where does that network status come from (synced payer provider directory, internal preferred-provider list, or payer-specific eligibility evidence)? | Situation | Approach | | :---- | :---- | | Named individual | Use `performer` → PractitionerRole when the practice/service context matters, or Practitioner for the individual alone. | | Pool or department | performer → Organization or HealthcareService when the referral is to a group or queue. | | Directory in Medplum | Maintain Practitioner, PractitionerRole, Organization, and HealthcareService; use Location for physical sites and reference-based recipient pickers. | | External directory | Sync or resolve selections to FHIR references at submit time via Bot or integration. | | Network-aware picker | Use authoritative payer directory or contract data for the provider, service, location, and effective date. Record unknown/stale status explicitly. A preferred-provider list or eligibility response alone does not establish network participation. | #### What clinical information and notes will travel with the referral? Decide what leaves with the referral, how much is automated versus curated at send, and which audiences see each part of the narrative. **Questions:** - What clinical material must travel with the referral (reason for referral, problems or diagnoses, key meds and allergies, recent labs or imaging, coverage), and what should be filled in automatically versus selected or edited by the sender before send? - Should internal staff-only notes appear on the referral, or only content intended for the receiving provider and the patient? | Situation | Approach | | :---- | :---- | | Receiver \+ patient messaging | Use `note` for shared clinical narrative and `patientInstruction` for patient-facing instructions. These fields do not enforce different audiences; keep internal notes in separately protected resources and exclude them from the package. | | Lean vs rich package | Link the clinical reason through `reasonReference` or `reasonCode`; use `supportingInfo` for selected evidence. Store files in Binary and link them through DocumentReference attachment URLs, without inline base64. | | Context at send | Let the sender review selected chart context. Retain the actual transmitted document or payload and its source versions so later chart edits do not change the send history. | #### Which channels and payloads will deliver the referral? Decide how referrals leave your product – whether recipients get structured exchange versus documents – and what you produce for each sending path. **Questions:** - Do any recipients accept referrals through structured exchange with your system, or only as documents (e.g. PDF or care-summary packages)? - What outbound channels do you plan to use (e.g. printed summary, secure email, fax, API)? - For each outbound channel you plan to use, what do you actually send or generate – structured data export, PDF, care-summary document, or a mix? - Is the outbound channel the same for every recipient (e.g. always fax) or does it vary per recipient – and if it varies, where does that preference live (recipient directory, referral type, picked at send time)? - For each channel, do you need to confirm delivery, retry on failure, and show send history to staff – and what counts as "successfully sent"? | Situation | Approach | | :---- | :---- | | FHIR-capable peer | Send or expose `ServiceRequest` and related resources via API; record transmission with `Communication` when useful for audit. | | No FHIR API (e.g., secure email, fax channels) | Generate a reviewed PDF, or use a validated C-CDA generator matching the exchange contract. Store the result as DocumentReference/Binary and link the sent package from Communication. | | More than one channel | Keep one `ServiceRequest` as source of truth; run separate send paths per channel without divergent clinical content. | ### How will incoming referrals be accepted? Decide how inbound referrals get created, reviewed, and matched to patients. **Questions:** - Which channels feed referrals into your product (e.g. partner API or health-information exchange, secure inbox or Direct Message, fax or scanned documents, imported files or care-summary packages, typed in by staff)? - For each channel, does a referral become active in your system automatically, or only after staff review or cleanup? - When inbound patient information is incomplete or inconsistent, how do you find or confirm the correct patient record? - After accepting (or declining) a new inbound referral, do you need to send an acknowledgment or response back to the originator – and through what path? - When the same referral arrives more than once (e.g. fax \+ secure inbox), how do you detect and handle duplicates? | Situation | Approach | | :---- | :---- | | Structured FHIR inbound | Validate ServiceRequest and related resources, resolve patient identity and local references, and preserve source identifiers. Track receiving review and acceptance with a Task. | | Structured documents or payloads you can parse (e.g. C-CDA XML, FHIR Bundles) | Bot or integration **parses** and **maps** fields → `ServiceRequest` (+ related resources); retain source as `DocumentReference` / `Binary`. Native structured interchange does **not** require OCR; image-only or bitmap PDF renditions may. | | Fax, scanned, or image-like documents | Retain the Binary and DocumentReference; extract fields with OCR and review uncertain identity or clinical content. Use a restricted intake Task until the patient is confirmed, then create or reconcile the ServiceRequest. | | Patient identity | Match trusted Patient identifiers with their namespaces and route ambiguity for review. Deduplicate referrals separately using the source referral identifier and conditional create. | ### Coordination and closure #### Who owns the work, and what gates authorization and scheduling? After a referral is created or accepted, decide how it gets to the right person, what authorization and coverage steps gate the work, and how it turns into an actual visit or service that ties back to the referral. *Note: Scheduling is covered in the [Scheduling Decision Guide](/docs/decision-guides/scheduling). This section stays at the level needed to align scheduling and authorization steps with the referral lifecycle.* **Questions:** - Who picks up a new referral first – an intake pool, a department queue, or an assigned coordinator (+ how does assignment logic work) – and how is ownership tracked and reassigned over time? - Does the referral need to confirm in-network status or covered benefits for the patient's plan before scheduling – and is that check separate from prior auth, or rolled into the same eligibility request? - Does the referral need to clear an authorization or coverage check before scheduling, and how do you track that authorization through to approval or denial? - When the referral becomes an actual visit (appointment, procedure, completed encounter), how do you tie that visit back to the originating referral? - Do you track aging or service-level expectations on open referrals (e.g. days since received, days pending an action), and how do staff find work that is overdue? - What happens after unsuccessful outreach, a patient decline, a no-show, or expiration? Who can stop attempts, and how is the referrer informed? - Which referral and authorization details must be available to billing, including effective dates and visit or unit limits? | Situation | Approach | | :---- | :---- | | Ownership and assignment | Set `Task.for` to the patient and `focus` to the ServiceRequest. Use `owner` for the accountable person/group and `performerType` for required roles; claim and reassign with version checks. | | Check in-network status / covered benefits | Use CoverageEligibilityRequest with `purpose: benefits` where supported. Evaluate network participation against authoritative payer data for the selected provider/service; do not assume a universal eligibility or clearinghouse response. | | Check whether prior auth is required | Use CoverageEligibilityRequest with `purpose: auth-requirements` where supported. Interpret `authorizationRequired` in the response's insurance items and supporting requirements under the payer contract. | | Submit a prior auth request | Use Claim with `use: preauthorization` and interpret ClaimResponse under the chosen payer API, implementation guide, or EDI mapping. Configure that integration explicitly; transport success does not mean approval. | | Authorization gate | Keep the Task on hold with an authorization-pending business stage until the required approval evidence is present and valid for the service/date. `ClaimResponse.outcome: complete` alone does not prove approval. | | Scheduling the work | See the [Scheduling Decision Guide](/docs/decision-guides/scheduling). From the referral side, link the resulting Appointment and downstream Encounter back to the referral via basedOn → ServiceRequest. | | Unsuccessful scheduling | Record outreach against the referral, assign retry/stop decisions, and close each work item according to its scope. Report the disposition to the referring team; see [unsuccessful scheduling](/docs/careplans/referrals/processing-and-coordination#follow-through-when-scheduling-does-not-succeed). | | Billing handoff | When required, use `Claim.referral` for the ServiceRequest and `Claim.insurance.preAuthRef` for the authorization number. Preserve payer-specific validity and service limits. | | Aging and SLA | Use Task history or explicit transitions for time in stage; `lastModified` may reflect unrelated edits. Follow the [Task planned-date convention](/docs/careplans/tasks#task-start--due-dates), range-aware search, and a defined pause policy. | #### Which lifecycle stages and changes must be tracked? Decide how you track a referral from request through completion – or cancellation – including naming those steps and handling cancel vs reroute. **Questions:** - Which milestones matter enough to show and operate on (e.g. sent, accepted, scheduled, declined, unable to schedule, completed, cancelled)? Which updates must reach the referrer? - How do you standardize labels for those milestones so reports, queues, and integrations all use the same meanings? - If a referral is cancelled or the recipient changes, do you revise the existing referral or create a new one? | Situation | Approach | | :---- | :---- | | Linear milestones | Use R4 `Task.status` for lifecycle and governed local `businessStatus` codes for workflow stages. Define the scope and completion evidence of each Task. | | External signals | Correlate and deduplicate callbacks before updating work. Delivery, receiving-team acceptance, service completion, and returned-result review are separate events. | | Cancel / reroute | For an unchanged clinical order, the authorized workflow can update `performer` and resolve old Tasks. For a replacement order, use a new ServiceRequest with `replaces` and end the old request appropriately. Preserve transmission history. | #### What must return, and what closes the referral loop? Decide what should come back after the referral is acted on, how those items attach to the right referral, and what counts as done for the referring side, the receiving side, or both. **Questions:** - After the referral goes out, what do you expect back – structured results (e.g. labs or imaging), a consult or visit summary, documents or letters, several of those, or nothing beyond tracking the referral itself? - When results or documents arrive, how do you attach them to the correct referral – including when human confirmation is needed? - What marks the referral complete for your operations – the referring organization, the receiving one, both, or different definitions for each? | Situation | Approach | | :---- | :---- | | Labs / imaging | `DiagnosticReport` with `basedOn` → `ServiceRequest`. | | Completed specialist visit | `Encounter` with `basedOn` → `ServiceRequest` when that models your workflow. | | Narrative / PDF reply | Use `DocumentReference.context.related` → ServiceRequest and an attachment URL → Binary. `context.encounter` is for Encounter or EpisodeOfCare. | | Linking | Manual selection or automated match using identifiers you control – define when human confirmation is required. | | Closure | End each Task with the appropriate `status` and disposition. Reconcile ServiceRequest completion from service evidence through the responsible authority. For closed-loop care, separately require returned-result review and assignment of follow-up. | ### Implementation Guides Use the decisions above to choose the next implementation guide: - [Creating and Capturing Referrals](/docs/careplans/referrals/creation-and-capture) - [Recipients and Referral Packages](/docs/careplans/referrals/recipients-and-packages) - [Sending Referrals and Tracking Delivery](/docs/careplans/referrals/transmition-and-tracking) - [Receiving and Triaging Referrals](/docs/careplans/referrals/receiving-and-triage) - [Processing and Coordinating Referrals](/docs/careplans/referrals/processing-and-coordination) - [Results and Closing the Loop](/docs/careplans/referrals/results-and-closure) - [Worked Referral Example](/docs/careplans/referrals/fhir-resource-examples) For assignment, delegation, and longitudinal workflows shared across the practice, use the [Care Coordination Decision Guide](/docs/decision-guides/care-coordination). --- ## Scheduling Decision Guide Use this guide to decide what can be booked, who can book it, and how appointments move through your workflow. _Companion to the [Scheduling](/docs/scheduling) docs._ Medplum provides availability calculation and atomic booking operations. Your application supplies business rules such as licensure checks, patient preferences, approval workflows, and integrations. The scheduling APIs are in [beta](/docs/compliance/alpha-beta); check the [beta changelog](/docs/scheduling/beta-changelog) when upgrading an existing implementation. This guide covers five areas: 1. **[Define what can be booked and by whom](#services-and-availability).** Choose services, schedulable resources, availability rules, and booking permissions. 2. **[Decide how resources are selected](#resource-selection-and-capacity).** Define eligibility and preferences; address resource combinations, shared capacity, and group visits where needed. 3. **[Define the booking lifecycle](#booking-and-changes).** Choose immediate booking versus holds or approval, then define conflicts, cancellations, and rescheduling. 4. **[Plan recurring visits, waitlists, and reminders](#recurring-visits-waitlists-and-reminders)** if your workflow needs them. 5. **[Define external-system responsibilities](#external-systems)** if you use telehealth, synchronize calendars, or work alongside another scheduler. Use the [questions below](#before-you-start) to establish your use case, then follow the relevant sections. Add requirements beyond these examples where needed. ### Before you start #### Who books, and on whose behalf? - Patient self-service - Staff booking for a patient - Provider managing their own appointments - System or API-driven booking - A combination of these _Why: determines authentication, delegated access, and which users can change availability or make exceptions ([Access](#who-may-book-administer-availability-and-make-exceptions), [Conflicts and overrides](#how-will-conflicts-refresh-and-controlled-overrides-work))._ #### What must be available for the appointment? - A clinician only - Several concurrent resources, such as a clinician, room, and device - A room or device without a clinician - A shared resource with capacity greater than one - A group session with several patients _Why: distinguishes multi-resource coordination from capacity and patient-specific workflow decisions ([Actors and Schedules](#which-actors-need-their-own-schedules), [Resource combinations](#which-resources-must-be-available-together), [Capacity and group visits](#how-will-shared-capacity-and-group-visits-be-modeled))._ #### Does the patient choose a person, or an eligible pool? - A named practitioner or care team - Any practitioner qualified for the service - First available, subject to preferences - Staff assignment after a request _Why: determines candidate selection, hard eligibility rules, preference ranking, and what “first available” promises ([Eligibility and preferences](#how-will-eligibility-and-preferences-determine-first-available), [Resource combinations](#which-resources-must-be-available-together))._ #### When is the appointment considered booked? - Immediately after the time is selected - After the patient completes another step - After staff approval or authorization review - After required participants respond _Why: determines whether to book immediately, reserve time with a hold, or keep a request without consuming capacity ([Booking and holds](#should-the-workflow-book-immediately-hold-time-or-await-approval))._ #### Is Medplum replacing a scheduler, or working alongside one? Identify the system of record for appointments, availability, cancellations, and external identifiers. Which existing appointments and blocks must be migrated? Which system can change them after launch? _Why: coexistence needs an explicit write authority and reconciliation policy. Existing scheduling data may also require a [beta migration](/docs/scheduling/beta-changelog) ([External calendars](#how-will-external-calendars-or-schedulers-exchange-changes))._ #### Who maintains the scheduling configuration? Who defines visit types, provider hours, resource assignments, holidays, and exceptions? Can providers edit their own hours? Who approves changes that affect appointments already booked? _Why: identifies configuration owners, administrative permissions, and change-management requirements ([Visit types](#what-are-the-visit-types-and-who-owns-their-parameters), [Actors and Schedules](#which-actors-need-their-own-schedules), [Hours and blocks](#when-is-booking-allowed-and-how-are-hours-and-blocks-maintained), [Access](#who-may-book-administer-availability-and-make-exceptions))._ ### Services and availability #### What are the visit types, and who owns their parameters? Decide what each bookable service means and which settings are shared versus actor-specific. Use a `HealthcareService` for each bookable appointment type. Its `SchedulingParameters` supply defaults; matching parameters on a `Schedule` override individual fields. See [Defining Availability](/docs/scheduling/defining-availability). **Questions:** - What distinguishes one visit type from another? - Which duration, start-time grid, buffers, and capacity settings are shared? - Which actors or sites need different settings, and who can change them? | Situation | Approach | | :--- | :--- | | Shared visit configuration | Put common parameters on HealthcareService. Put shared weekly hours in its native `availableTime` field. | | Per-actor differences | Set only the differing Schedule parameters, with a `service` reference to the HealthcareService. Unset fields inherit. | | Schedule offers every service | Explicitly opt in with the [all-service marker](/docs/scheduling/defining-availability#offering-all-service-types). It includes future services; empty `serviceType` offers none. Keep the specific HealthcareService on each Appointment | | Different hours by site | Consider location-specific HealthcareService offerings and per-service Schedule overrides. Separate services by location only when the distinction matters to scheduling. | | Multiple resources booked together | Keep effective duration, alignment interval, offset, and alignment timezone equal across the required Schedules. Buffers, hours, and capacity can vary. | | Retiring a visit type | `HealthcareService.active = false` prevents finding, booking, holding, and confirming appointments for it. Existing appointments remain; cancellation is still allowed. Plan for outstanding holds. | | Retiring a Schedule | `Schedule.active = false` prevents new use and confirmation of its holds. Existing appointments remain; cancel them or reschedule onto active destination Schedules, omitting the retired Schedule | **Reference matching matters:** `Schedule.serviceType` uses the service-reference extension within a CodeableConcept. Schedule parameter overrides separately identify their service with a `service` sub-extension. A matching code alone does not establish these links, and an override without a service pointer is ignored. The all-service marker controls service eligibility, not hours. Configure hours on every offered HealthcareService or through per-service Schedule overrides; missing availability defaults to always available. Review this whenever another service is added. #### Which actors need their own Schedules? Decide which resources consume time and which merely describe eligibility or location. Medplum requires exactly one actor on each Schedule. Consolidating a practitioner's availability on one Schedule is the recommended starting point; the server does not enforce one Schedule per actor or automatically synchronize separate calendars for the same person. **Questions:** - Which practitioners, rooms, and devices must be reserved? - Is a Location a bookable resource, a site of service, or a jurisdiction? - Are resource requirements simultaneous, or different phases of a procedure? | Situation | Approach | | :--- | :--- | | Clinician availability | Use a Practitioner actor. Use PractitionerRole for service/jurisdiction eligibility rather than splitting the person's calendar by license. | | Individually bookable room or device | Give the Location or Device its own Schedule and include it in the appointment search and booking. | | Facility or jurisdiction used for filtering | Model the association for candidate selection; do not assume a Location reference reserves time or checks capacity. | | Record the appointment's site | Store a Location reference in `Appointment.supportingInformation` using `toAppointmentSiteReference` from `@medplum/core`; read it with `getAppointmentSite`. This identifies the site separately from a booked room and does not reserve capacity. | | Same person at several sites | Keep the actor consistent and vary service-specific availability. Separate calendars can otherwise permit conflicting bookings. | | Resources needed for different phases | Define a separate orchestration design. A concurrent multi-Schedule appointment uses a common booking interval, not independently timed resource phases. | #### When is booking allowed, and how are hours and blocks maintained? Decide when booking is allowed and how exceptions are maintained. Use implicit availability, not pre-generated open Slots. Persist Slots for booked, held, or blocked time. **Questions:** - Are weekly hours shared, actor-specific, or service-specific? - Which time zone defines local hours and which defines the start-time grid? - What are the maximum booking horizon, minimum notice, and exception rules? - Who enters closures, and what happens to appointments already booked? | Situation | Approach | | :--- | :--- | | Shared hours with actor overrides | HealthcareService `availableTime` supplies defaults. A Schedule availability override replaces those hours, rather than intersecting with them. | | Prep or cleanup time | Set `bufferBefore`/`bufferAfter`; booking reserves exclusive buffer Slots. Include buffers when assessing usable appointment times. | | Fixed appointment grid | Set alignment interval and offset deliberately. The default is 60 minutes with offset zero, not automatic back-to-back placement. | | Local hours and daylight saving time | Resolve availability timezone from Schedule parameters, then service parameters, then the actor extension. Configure `alignmentTimezone` separately if the grid must follow local midnight; its default is `Etc/UTC`. Test relevant DST transitions. | | One-off absence or holiday | Create `busy-unavailable` Slots on affected Schedules. Omit Slot `serviceType` to block all services; set it only for an intentionally service-specific block. | | Facility closure | Include the facility as a required schedulable resource, or apply blocks to each affected Schedule. A facility block does not automatically propagate to practitioners. | | Maximum booking window | Use `Schedule.planningHorizon`. A rolling horizon requires application maintenance. | | Minimum lead time or daily/weekly caps | The scheduling server does not enforce these policies. Implement and validate them in the application's controlled booking path, not just search filters. | **Defaults need explicit review:** without configured availability, time defaults to always available. Without a resolvable availability timezone, scheduling fails. `availability.notAvailableTime` is not currently processed; use blocked Slots for exceptions. A new block does not itself resolve existing appointments, so define the notification and rescheduling workflow. #### Who may book, administer availability, and make exceptions? Decide who may see schedules and patient details, book for someone else, change availability, and make exceptions. Use [AccessPolicy and ProjectMembership](/docs/access/access-policies) for data access; use the [Access Control Decision Guide](/docs/decision-guides/access-control) for the broader authorization model. **Questions:** - Can patients discover availability without reading other patients' appointments? - Which staff can book or cancel for which patients and organizations? - Who can change shared defaults, provider hours, or blocked time? | Situation | Approach | | :--- | :--- | | Patient self-service | Grant only the resource access required by the supported operations and the patient's workflow. Test with patient credentials, including denied access cases. | | Delegated staff booking | Scope access by role and organization. Preserve the initiating user and reason for delegated or exceptional actions in the audit design. | | Rescheduling permission | In addition to booking access, `$reschedule` needs Appointment read/update and Slot read/delete access. The existing Slots and their Schedules must be readable. Test these permissions explicitly; permission to book does not imply permission to move an appointment. | | Required business checks beyond access rules | Route booking through a controlled server-side workflow, and restrict alternate write paths that could bypass those checks. | | Sensitive group or cross-team appointments | Review resource-level visibility before putting multiple patients or teams on a shared Appointment. | Candidate filtering and hidden UI controls are not authorization. Do not assume permissions on a Schedule automatically confer the intended permissions on its Slots or Appointments. A post-write Subscription Bot can trigger follow-up work, but cannot serve as a pre-commit rejection of the original write. ### Resource selection and capacity #### How will eligibility and preferences determine first available? Decide which actors are eligible before searching, then how to rank acceptable results. Scheduling operations do not infer licensure, enrollment, patient preferences, or clinical suitability. See [Matching Patients to Providers by State-by-State Licensure](/docs/scheduling/state-by-state-licensure). **Questions:** - Is the request for a specific practitioner, a class of practitioners, or any eligible practitioner? - Which requirements are hard constraints, and which preferences can be relaxed with patient agreement? - Are language, practitioner gender, continuity, location, or day/time preferences relevant? - Does “first available” mean earliest overall, earliest preferred, or earliest within a bounded search? - How are equally suitable options selected, and what happens when some searches fail? | Situation | Approach | | :--- | :--- | | Licensure or service eligibility | Resolve allowed actors through the application's rules before `$find`, and validate eligibility again in the booking workflow. Model a practitioner's service role with its jurisdiction Locations; multiple jurisdictions may share one role. | | Hard patient requirement | Remove incompatible candidates or times before presenting an offer. Do not silently relax it when no result exists. | | Soft preference | Rank eligible results and make alternatives explicit. Keep preference ranking separate from access and licensure rules. | | Named practitioner | Search that practitioner's Schedule plus any other required resources. | | First available from a pool | Search eligible candidates or combinations over a defined horizon, merge results, and apply a stable tie-break rule. Expand the horizon explicitly when needed. | | Partial results | Distinguish “no availability” from an incomplete or failed search. Do not claim globally earliest availability when only part of the candidate set was searched. | `$find` accepts explicit Schedules, not a patient-preference or ranking specification. Its search window is limited to 31 days per request; larger horizons require multiple requests. Result limits and candidate-combination limits must also be considered when defining first-available behavior. #### Which resources must be available together? Decide which resources are required together and where alternatives are allowed. Repeating `schedule` in [`$find`](/docs/scheduling/appointment-find) requests the intersection of all those Schedules, not a choice among them. **Questions:** - Does the appointment need one practitioner or several? - Are the room and device fixed, or can any eligible one be used? - How large can the candidate pool become? | Situation | Approach | | :--- | :--- | | Named clinician and named room | Send both Schedules in one `$find` call. Book the resulting proposal with one `$book` or `$hold` operation. | | Either clinician A or B, plus one room | Search each eligible clinician/room combination, then merge options. Passing both clinicians together would reserve both. | | Two clinicians required | Include both Schedules in the same search and booking. Each must support the service and compatible common parameters. | | Large pools | Narrow eligibility before enumerating combinations, bound the search, and define how partial results are displayed. | Use the server's multi-Schedule booking operation for the selected combination. Sequential independent bookings can leave only part of the required appointment reserved. #### How will shared capacity and group visits be modeled? Decide whether concurrency represents deliberate overbooking, interchangeable resource capacity, or patients attending a shared session. These are different modeling decisions. See [Overbooking](/docs/scheduling/defining-availability#overbooking). **Questions:** - Must each chair or room be individually assigned, or are they interchangeable? - Does each patient need independent cancellation, access, clinical documentation, and billing? - Does a provider participate in every concurrent booking? | Situation | Approach | | :--- | :--- | | Exclusive resource | Keep the default `slotCapacity` of 1. | | Deliberate overlapping appointments | Set `slotCapacity` on the service or Schedule and continue using `$book`/`$hold`. All required resources must have capacity. | | Interchangeable stations | A shared Schedule with capacity can represent the pool. Use individual actors when assignment, equipment differences, or station-specific blocks matter. | | Several patients in a session | Prefer patient-specific Appointments when privacy and per-patient lifecycle matter; define explicit session linkage and capacity. A single multi-patient Appointment requires a separate participant-count policy. | Capacity counts overlapping booking Slots, not the number of Patient participants. The strictest capacity among overlapping bookings applies, and existing Slots retain the capacity stamped when booked. Changing configuration does not rewrite those Slots. Buffers remain exclusive and can prevent otherwise permitted overlaps. **Costly modeling choice:** a shared multi-patient Appointment couples access and lifecycle. FHIR R4 has no `Appointment.partOf`; define any session grouping deliberately rather than inventing that field. ### Booking and changes #### Should the workflow book immediately, hold time, or await approval? Decide whether to consume capacity immediately, reserve it temporarily, or keep an unreserved request. Approval and prior-authorization workflows are application responsibilities, not automatic effects of an Appointment status. **Questions:** - Does the time need protection while details or approvals are collected? - Who approves, what evidence is required, and how long may a hold remain? - Who releases abandoned or rejected requests? | Situation | Approach | | :--- | :--- | | Immediate booking | `$find` → [`$book`](/docs/scheduling/appointment-book). Creates a booked Appointment and its busy/buffer Slots atomically. | | Reserve while awaiting approval | `$find` → [`$hold`](/docs/scheduling/appointment-hold) → [`$confirm`](/docs/scheduling/appointment-confirm). A hold creates a pending Appointment and busy-tentative Slots that consume capacity. | | Rejected or expired hold | Use [`$cancel`](/docs/scheduling/appointment-cancel) to release it. Define expiry tracking and cleanup in the application; `$hold` has no built-in TTL. | | Request without reserving time | Track the request separately, such as in a Task. Search and book when ready; availability may have changed. | | Participant acceptance required | Capture responses and implement the rule that permits confirmation. An AppointmentResponse does not automatically confirm a hold. | For a patient appointment, add the selected Patient to the proposal's participants before booking or holding; `$find` supplies the scheduled actors, not the patient. Do not call `$book` to confirm an existing hold. Do not use `tentative` as an Appointment status; it is a participant status. Make confirmation and expiry processing coordinate so an approval cannot race an independent cleanup workflow unnoticed. #### How will conflicts, refresh, and controlled overrides work? Decide how the user experiences a changed result and who can bypass ordinary scheduling rules. `$find` is a snapshot, not a reservation. `$book`, `$hold`, and `$reschedule` recheck availability transactionally, including capacity and buffers. **Questions:** - Is refresh-on-action sufficient, or does the workflow need live updates? - What happens when another user books the last available capacity? - Which exceptions are allowed, who authorizes them, and how are they audited? | Situation | Approach | | :--- | :--- | | Ordinary booking conflict | Refresh search results and ask the user to select another option. Do not present an unsuccessful booking as confirmed. | | Live calendar experience | Use bounded polling or appropriately scoped [Subscriptions](/docs/subscriptions). Refresh affected results; live updates do not replace transactional booking validation. | | Ambiguous response after a network failure | Reconcile whether the appointment was created before retrying. Avoid blind retries that can create duplicate bookings. | | Routine capacity greater than one | Configure capacity rather than implementing an override path. | | Privileged exception | Define a controlled write path, required reason, access restrictions, consistency checks, and downstream notification. Direct FHIR writes do not inherit the scheduling operation's validation. | The React workspace's `canBypassSchedulingRules` option exposes controls; it does not enforce permission. A transaction can keep custom Appointment/Slot writes together, but does not by itself reproduce `$book`'s conflict checks. Ensure transaction support is enabled for cross-referencing writes; do not rely on a batch fallback for atomicity. #### How will cancellation, rescheduling, and clinical transitions work? Decide which changes are allowed after booking, how reasons are recorded, and when an appointment becomes a clinical encounter. **Questions:** - Who can cancel or move an appointment, and with what notice? - Must rescheduling preserve Appointment identity and downstream references? - Which arrival, no-show, and completion states are needed? - When is the Encounter created, and who owns that automation? | Situation | Approach | | :--- | :--- | | Cancel a booked or pending appointment | Use `$cancel`, optionally with a structured `cancelationReason`. It preserves the cancelled Appointment and deletes its linked Slots, including buffers, atomically. | | Find a replacement time or resource | Pass `ignore-appointment=Appointment/{id}` to `$find` to exclude this appointment's Slots, including buffers, from availability calculations. Other appointments still block. Commit the move with `$reschedule`; `$book` and `$hold` do not ignore the original Slots. | | Move time or resources | Use [`$reschedule`](/docs/scheduling/appointment-reschedule) for a booked or pending Appointment. Supply the new `start` and the complete set of destination Schedules. It preserves Appointment identity and status, reconciles scheduled participants, and replaces Slots atomically. If the new time is unavailable, the original booking remains intact. | | Change visit type | `$reschedule` retains the original service type and requires exactly one HealthcareService reference on the stored Appointment. Changing visit type requires cancel-and-book, with explicit failure/recovery handling and a new Appointment identity. | | Link a replacement appointment | Use an explicit application linkage or Provenance model. R4 `Appointment.basedOn` references ServiceRequest, not another Appointment. | | Arrival and encounter creation | Define application transitions and create the Encounter at the chosen event, linking through `Encounter.appointment`. Make automation safe to retry. | | Participant accept/decline | Use `Appointment.participant.status` and, where needed, AppointmentResponse. Keep response handling distinct from the overall Appointment status. | | No-show or completion | Define the operational rule and accountable actor. `$book` does not automatically manage later clinical states. | Confirm that your server version supports `$reschedule` before choosing this workflow. It derives duration and buffers from the scheduling configuration; omitted Schedules are removed from the appointment. Patients and other non-scheduled participants are preserved. Slot IDs change, so integrations must handle Slot deletion and creation. Site metadata in `supportingInformation` stays unchanged; a move to another site needs an explicit metadata update in the application's workflow. Do not equate an SMS acknowledgment with confirmation of a held booking, or mutate only `Appointment.start`/`end` to reschedule while leaving its Slots behind. Notify dependent systems whenever the time, actors, or status changes. ### Recurring visits, waitlists, and reminders #### How will recurring appointment series be created and managed? Decide which recurrence patterns you need and how the series is managed after creation. Medplum supports weekly series of 2–6 occurrences at the same local time, with all-or-none booking or holding. Other patterns and series-wide lifecycle changes need application orchestration. **Questions:** - Is the series finite, or maintained over a rolling horizon? - Must every occurrence be available before the series is accepted? - Can a user change one occurrence, all future occurrences, or the entire series? - Does recurrence follow local wall-clock time across DST changes? | Situation | Approach | | :--- | :--- | | Weekly series of 2–6 occurrences | Use [`$find` with `occurrence-count`](/docs/scheduling/appointment-find#finding-a-recurring-series), then pass the selected series proposal, preserving its recurrence template and adding the Patient participant, to one `$book` or `$hold`. If any occurrence is unavailable, none are created | | Series driven by an actual service order | Each occurrence may reference its ServiceRequest through `Appointment.basedOn`. Define separate series identity if one order can generate several series. | | Series identity | `$book` and `$hold` generate a shared series identifier and per-occurrence recurrence IDs. Use these to find the series; no clinical order is needed solely for grouping | | Other recurrence patterns or a rolling horizon | Implement a separate orchestration workflow. Define grouping, exceptions, and partial-success recovery; multiple independent `$book` calls are not atomic as a series | | Series-wide confirmation, cancellation, or changes | Orchestrate per-occurrence operations with recovery and notification. Atomic creation does not imply atomic series-wide lifecycle updates; coordinate hold expiry with confirmation | The recurring search window covers the first occurrence and is limited to one local week (normally 7 days). All selected Schedules must share a timezone. The series preserves local time across DST changes and rejects occurrences at nonexistent local times. FHIR R4 recurrence uses documented cross-version extensions, not native R5 fields. Preserve the template returned by `$find` and use the series constants from `@medplum/core`; do not use `Appointment.partOf` or a CarePlan reference in `Appointment.basedOn`. See [Recurring Booking](/docs/scheduling/appointment-book#booking-a-recurring-series) and [Recurring Holds](/docs/scheduling/appointment-hold#holding-a-recurring-series) for supported templates and identifiers. #### How will vacancies become waitlist offers or bookings? Decide whether a vacancy triggers an offer, a hold, or an automatic booking. Waitlist ranking and promotion require application orchestration. **Questions:** - What service, candidates, time range, and preferences does the patient accept? - How are priority and fairness determined? - How long is an offer valid, and does it reserve capacity? | Situation | Approach | | :--- | :--- | | Track unmet demand | Use a workflow record such as Task containing the request criteria and patient reference. | | Notify when time opens | Re-run eligibility and availability checks, then send an offer. Notification alone does not reserve time. | | Offer with a reservation | Use `$hold` and an application-managed expiry policy. Coordinate competing offers and cleanup. | | Automatic promotion | Confirm the patient's consent and criteria, revalidate eligibility, and use `$book`; handle a competing booking as a normal conflict. | #### When are reminders sent, and what do replies mean? Decide the channels, timing, and meaning of responses. Reminder acknowledgment, participant acceptance, and booking confirmation are separate events. **Questions:** - Which channels and timing rules apply, and in whose time zone? - Can replies acknowledge, cancel, or request a change? - How are reminders suppressed after cancellation or rescheduling? | Situation | Approach | | :--- | :--- | | Scheduled outbound reminder | Use application scheduling/Bots and a channel provider. Track delivery attempts and deduplicate by appointment, reminder type, and relevant appointment version/time. | | Two-way response | Authenticate or otherwise validate the response context and route it to the appropriate workflow or operation. | | Appointment changes | Invalidate obsolete reminders and schedule replacements. Recheck current state before sending. | Use the [Messaging & Communications Decision Guide](/docs/decision-guides/messaging) for channel, consent, and delivery design. ### External systems #### When will telehealth meetings be provisioned and updated? Decide when to create a virtual meeting and how participants receive access. Video provisioning is an integration, not part of `$book`. **Questions:** - Is the visit virtual, in-person, or selectable per booking? - Is the meeting created at booking, approval, or shortly before the visit? - Who can read the join details, and what happens when the visit changes? | Situation | Approach | | :--- | :--- | | Provision at booking or confirmation | Trigger an idempotent integration after the chosen state is committed. Keep failures visible without implying the booking failed. | | Provision just before the visit | Recheck the Appointment state before creating the meeting and sending access details. | | Store join details | Use a documented R4-compatible extension or linked resource, with appropriate access restrictions. | | Cancel or move the appointment | Reconcile the external meeting and invalidate obsolete links or invitations as required. | #### How will external calendars or schedulers exchange changes? Decide whether the integration exports appointments, imports busy time, or permits another system to change bookings. Establish one write authority per kind of data. **Questions:** - Is the destination a personal calendar, patient calendar feed, or another clinical scheduler? - Do external events block availability, and do edits move Medplum appointments? - How are cancellations, recurrence exceptions, outages, and reconciliation handled? | Situation | Approach | | :--- | :--- | | Outbound appointment mirror | Publish selected appointment details and retain external identifiers. Minimize patient information exposed to personal calendars. | | External busy-time import | Maintain `busy-unavailable` Slots for relevant events in the booking horizon. Handle updates and deletion as well as creation, with stable external identifiers and conditional writes. | | Bidirectional appointment editing | Specify conflict ownership, echo suppression, and reconciliation. Route accepted changes through the controlled scheduling workflow. | | Patient calendar feed | Provide a patient-scoped, revocable access mechanism and a clear policy for sensitive details. | An external calendar event does not automatically create a Medplum conflict. Imports and reconciliation must maintain the blocking Slots. Use the [Data Migration Decision Guide](/docs/decision-guides/data-migration) when replacing or running alongside another scheduler. ### Reference Implementation For a reference implementation of a complex scheduling interface, see [`@medplum/react-scheduling`](https://github.com/medplum/medplum/tree/main/packages/react-scheduling), including SchedulingWorkspace, appointment search/booking components, MultiCalendar, and ScheduleAvailabilityEditor. The package is currently Alpha and subject to breaking changes; host applications supply business rules, authorization, and integrations. Explore the components in [Storybook](https://storybook.medplum.com/). ### Before You Implement - Record the chosen service, actor, availability, and access models, with an owner for each. - Separate server-supported behavior from custom workflows and unresolved product requirements. - Test booking conflicts, capacity and buffers, time zones, expiry/cancellation races, and denied access using representative user roles. - Define recovery for partial series bookings, failed integrations, and ambiguous responses. - Confirm the server/package versions in use and review the scheduling beta changelog before launch or upgrade. - Assign an owner and acceptance criteria to every unresolved answer and every custom scheduling rule. --- ## DICOM with the Medplum Agent Imaging equipment does not speak DICOMweb. Modalities, workstations, and legacy PACS speak **DIMSE** — the DICOM message service over raw TCP — and they speak it on a hospital network with no route to the public internet. The [Medplum Agent](/docs/agent) bridges that gap: it runs inside the firewall, presents itself to the modality as an ordinary DICOM Storage SCP, and forwards each received instance to Medplum over an outbound HTTPS connection. ```mermaid flowchart LR M["Modality
(CT, MR, CR, US)"] -- "C-STORE over TCP" --> A["Medplum Agent
dicom://0.0.0.0:8104"] A -- "HTTPS (outbound only)" --> S["Medplum Server"] A -- "notification" --> B["Bot"] ``` The Agent needs no inbound firewall rule. From the network's perspective it is a device that listens on a local port and makes outbound HTTPS calls, which is what makes it deployable in environments that will not expose a PACS to the internet. ### Supported DIMSE operations | Operation | Supported | Notes | | --------- | --------- | ------------------------------------------------------------ | | `C-ECHO` | Yes | Always answers success. Use it to verify connectivity first. | | `C-STORE` | Yes | Stores the instance and notifies a Bot. | | `C-FIND` | No | | | `C-GET` | No | | | `C-MOVE` | No | | The Agent accepts every presentation context and transfer syntax the calling AE proposes, and negotiates a maximum PDU length of 64 KB. In practice this means a modality's default configuration usually associates successfully without transfer syntax tuning. ### Configuring a DICOM channel A DICOM channel is configured the same way as any other Agent channel — an [`Endpoint`](/docs/api/fhir/resources/endpoint) describing what to listen on, referenced from a channel entry on the [`Agent`](/docs/api/fhir/medplum/agent) resource. See [Intro to Medplum Agent](/docs/agent) for the full setup, including the `Bot` and `ClientApplication` that go with it. What makes the channel a DICOM channel is the `dicom://` scheme on `Endpoint.address`: ```json { "resourceType": "Endpoint", "status": "active", "name": "CT Scanner", "connectionType": { "system": "http://terminology.hl7.org/CodeSystem/endpoint-connection-type", "code": "dicom-stow-rs" }, "payloadType": [ { "coding": [ { "system": "http://terminology.hl7.org/CodeSystem/endpoint-payload-type", "code": "any" } ] } ], "address": "dicom://0.0.0.0:8104?storage=dicomweb" } ``` The Agent selects the channel implementation from the address scheme alone — `connectionType` is descriptive metadata for humans and reporting, not something the Agent interprets. Port 104 is the registered DICOM port, but it is privileged on Linux; 8104 and 11112 are the usual unprivileged choices. Configure the modality to send to the Agent host at that port. The Agent accepts any Called AE Title, and records both the calling and called AE titles on the notification it sends to the Bot. ### Storage modes The `storage` query parameter controls where a received instance ends up. | Mode | What happens | Requires | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------- | | `binary` _(default)_ | The instance is uploaded as a FHIR [`Binary`](/docs/api/fhir/resources/binary), and a reference to it is included in the Bot payload. | Any server version | | `dicomweb` | The instance is sent to the server's [STOW-RS endpoint](./dicomweb-api.md#stow-rs-store-instances), which files it into `DicomStudy`, `DicomSeries`, and `DicomInstance` resources. No `Binary` is created by the Agent. | Medplum Server \> 5.1.27, Agent \> 5.1.28 | `binary` remains the default so that a DICOM channel configured before DICOMweb existed keeps working unchanged. **New deployments that want studies in the DICOM data model should set `storage=dicomweb`.** In that mode the payload delivered to the Bot has no `binary` field — the study is addressed through the DICOM resources the server created instead. An unrecognized `storage` value logs a warning and falls back to `binary`, so a typo cannot silently point a channel at an endpoint the server may not have. Against a server without DICOMweb support, the STOW-RS request 404s and the `C-STORE` fails with a processing failure status. Changing the storage mode takes effect on [`Agent/$reload-config`](/docs/agent/reload-config) without a restart, and without rebinding the port. ### What the Bot receives Every `C-STORE` produces a notification to the channel's target `Bot`, regardless of storage mode: ```json { "association": { "callingAeTitle": "CT_SCANNER_1", "calledAeTitle": "MEDPLUM" }, "dataset": { "00080018": { "vr": "UI", "Value": ["1.2.840.113619.2.55.3.12345"] }, "00080060": { "vr": "CS", "Value": ["MR"] }, "0020000D": { "vr": "UI", "Value": ["1.2.840.113619.2.55.3.99999"] } }, "binary": { "reference": "Binary/0195f2c1-..." } } ``` - `association` carries the AE titles from the DIMSE association, which is how you tell one modality from another when several send to the same channel. - `dataset` is the instance's DICOM JSON with `PixelData` `(7FE0,0010)` removed — enough to route, match a patient, or build an order reconciliation, without shipping the image through the Bot. - `binary` is present only in `binary` storage mode. A minimal Bot that logs each arrival: ```ts import { BotEvent, MedplumClient } from '@medplum/core'; interface DicomNotification { association: { callingAeTitle?: string; calledAeTitle?: string }; dataset: Record; binary?: { reference?: string }; } export async function handler(medplum: MedplumClient, event: BotEvent): Promise { const { association, dataset } = event.input; const sopInstanceUid = dataset['00080018']?.Value?.[0]; console.log(`Received ${sopInstanceUid} from ${association.callingAeTitle}`); } ``` In `dicomweb` mode this is the natural place to reconcile imaging against the chart — look up the `DicomStudy` the server just created, match `patientId` against your MRN identifier system, and link the two. See [Relationship to FHIR ImagingStudy](./data-model.md#relationship-to-fhir-imagingstudy). ### Testing a channel Verify connectivity before involving the modality. With [dcmtk](https://dicom.offis.de/dcmtk.php.en) installed: ```bash # C-ECHO — verifies the association handshake only echoscu -v localhost 8104 # C-STORE — sends a file storescu -v localhost 8104 MRBRAIN.DCM ``` A successful `C-STORE` in `dicomweb` mode should be followed by a new `DicomStudy` in your project. If the `C-STORE` returns a processing failure, check the Agent's **channel log** — the main Agent log deliberately excludes message content, so DICOM transfer detail lands in the channel log, which [may contain PHI](/docs/agent/configuration#channel-logger). To test without a modality or dcmtk at all, skip DIMSE entirely and use [`medplum dicomweb stow`](./cli.md). ### See also - [Intro to Medplum Agent](/docs/agent) - [Agent Features](/docs/agent/features) — version matrix for `storage=dicomweb` and other channel options - [Agent Troubleshooting](/docs/agent/troubleshooting) - [DICOMweb API](./dicomweb-api.md) --- ## DICOM with the Medplum CLI The [Medplum CLI](/docs/cli) can store a DICOM file through STOW-RS directly, which makes it the fastest way to get imaging into a project — no Agent, no modality, no hand-built multipart body. ```bash npm install --global @medplum/cli medplum login medplum dicomweb stow MRBRAIN.DCM ``` ### `medplum dicomweb stow` ``` medplum dicomweb stow [--batch-size ] ``` Reads DICOM Part 10 files, wraps them in a `multipart/related` body with `Content-Type: application/dicom`, and posts them to `/dicomweb/studies` on the configured server. Files are streamed from disk into the request rather than read into memory first. On success the STOW-RS response of each request is printed — a DICOM JSON dataset containing a Referenced SOP Sequence naming the SOP Class UID and SOP Instance UID of each stored instance. The command accepts the CLI's standard [authentication](/docs/cli#authentication) and server options — stored credentials from `medplum login`, `MEDPLUM_CLIENT_ID` / `MEDPLUM_CLIENT_SECRET` environment variables, or `--client-id` / `--client-secret` flags. Set `MEDPLUM_BASE_URL` to target a self-hosted server. #### Files, directories, and patterns Each argument can be a file, a directory, or a glob pattern, and you can mix them freely: ```bash medplum dicomweb stow MRBRAIN.DCM # One file medplum dicomweb stow ./study # Every DICOM file in the tree, recursively medplum dicomweb stow ./study/*.dcm # Expanded by your shell medplum dicomweb stow './study/**/*.dcm' # Quoted, so the CLI expands it ``` An unquoted pattern such as `*.dcm` is expanded by the shell before the CLI ever runs, so it arrives as an ordinary list of file names. Quote the pattern to have the CLI expand it instead — which is also how to pass patterns on Windows, where the shell does no expansion at all. :::caution Quote `**` patterns **Always quote a pattern containing `**`.** bash only treats `**` as "any number of directories" when `globstar` is enabled, and it is off by default. Unquoted, `./study/**/*.dcm` expands to *exactly one* directory level, so a three-level study uploads only its middle level — and because the shell collapsed the pattern before the CLI started, nothing can detect this and warn you. Quoted, the CLI expands it and `**` recurses to any depth, including zero, so top level files match too. Passing the directory itself avoids the question entirely: ```bash medplum dicomweb stow ./study ``` ::: Patterns are matched case-insensitively, since `.dcm` and `.DCM` are both common. Directories are searched by content rather than by name, so their casing never matters. Directories are searched recursively, to any depth. When expanding a directory or a pattern, files that are not DICOM instances are skipped, so pointing at an export folder does not try to upload its README. A file counts as DICOM if it carries the `DICM` prefix at byte 128, or if it begins with a `(0002,xxxx)` File Meta Information tag — the two forms the server's reader recognizes structurally, including the extensionless file names common on modality exports and DICOM media. The server also accepts raw datasets carrying no File Meta Information, which have no header to test for, so those are recognized by a `.dcm`, `.dicom`, or `.ima` extension. `DICOMDIR` index files are always skipped, and the count of skipped files is printed. A file named explicitly on the command line is always sent, with no such filtering. #### Batching Instances are sent in batches of 25 per STOW-RS request; use `--batch-size` to change that. Batching keeps a large upload from riding on one long-lived request, so a failure costs one batch rather than the whole study. Studies and series are created conditionally on their DICOM UIDs, so instances still collect under a single `DicomStudy` and its series no matter how they are split across requests. ### Verifying the upload Instances land in the [DICOM data model](./data-model.md) immediately; pixel data extraction happens in the background. Check with an ordinary FHIR search: ```bash medplum get 'DicomStudy?_count=5' medplum get "DicomSeries?study=DicomStudy/$STUDY_ID" ``` Or over DICOMweb: ```bash curl https://api.medplum.com/dicomweb/studies \ -H "Authorization: Bearer $MEDPLUM_TOKEN" ``` ### See also - [Medplum CLI](/docs/cli) - [DICOMweb API](./dicomweb-api.md) - [OHIF Viewer](./ohif-viewer.md) — to see what you just uploaded at [viewer.medplum.com](https://viewer.medplum.com) --- ## DICOM Data Model DICOM organizes imaging into a three-level hierarchy — **study**, **series**, **instance** — and Medplum models that hierarchy directly with three resource types rather than flattening it into FHIR `ImagingStudy`. The DICOM information model and the FHIR one disagree in enough places that a lossy translation at ingest time would throw away exactly the attributes a viewer needs to render the study. Storing the DICOM shape natively means a DICOMweb response can be reconstructed faithfully, while the resources remain searchable and access-controlled like anything else in the project. ```mermaid flowchart TD Study["DicomStudy
studyInstanceUid"] Series["DicomSeries
seriesInstanceUid"] Instance["DicomInstance
sopInstanceUid"] Raw["Binary
original .dcm file"] Pixels["Binary[]
pixel data, one per frame"] Study --> Series Series --> Instance Instance -- "raw" --> Raw Instance -- "pixelData" --> Pixels ``` ### Resource types #### DicomStudy One [`DicomStudy`](/docs/api/fhir/medplum/dicomstudy) per DICOM Study Instance UID. Created conditionally on `studyInstanceUid`, so instances arriving over separate uploads collect under one study. | Field | DICOM tag | Notes | | ------------------------------- | ------------- | ------------------------------------------------------- | | `studyInstanceUid` | `(0020,000D)` | Required. The conditional-create key. | | `studyId` | `(0020,0010)` | | | `studyDate` | `(0008,0020)` | Converted `YYYYMMDD` → `YYYY-MM-DD` | | `studyTime` | `(0008,0030)` | Converted `HHMMSS` → `HH:MM:SS` | | `accessionNumber` | `(0008,0050)` | | | `instanceAvailability` | `(0008,0056)` | | | `modalitiesInStudy` | `(0008,0061)` | | | `referringPhysiciansName` | `(0008,0090)` | | | `timezoneOffsetFromUtc` | `(0008,0201)` | | | `patientName` | `(0010,0010)` | The `Alphabetic` component of the DICOM person name | | `patientId` | `(0010,0020)` | A DICOM string, **not** a reference to a FHIR `Patient` | | `patientBirthDate` | `(0010,0030)` | Converted `YYYYMMDD` → `YYYY-MM-DD` | | `patientSex` | `(0010,0040)` | | | `numberOfStudyRelatedSeries` | `(0020,1206)` | | | `numberOfStudyRelatedInstances` | `(0020,1208)` | | #### DicomSeries One [`DicomSeries`](/docs/api/fhir/medplum/dicomseries) per Series Instance UID, created conditionally on `seriesInstanceUid`. | Field | DICOM tag | | --------------------------------- | ------------------------------------ | | `study` | Reference to the parent `DicomStudy` | | `seriesInstanceUid` | `(0020,000E)` | | `seriesNumber` | `(0020,0011)` | | `modality` | `(0008,0060)` | | `seriesDescription` | `(0008,103E)` | | `timezoneOffsetFromUtc` | `(0008,0201)` | | `numberOfSeriesRelatedInstances` | `(0020,1209)` | | `performedProcedureStepStartDate` | `(0040,0244)` | | `performedProcedureStepStartTime` | `(0040,0245)` | #### DicomInstance One [`DicomInstance`](/docs/api/fhir/medplum/dicominstance) per stored SOP instance. Unlike study and series, instances are **not** created conditionally. | Field | DICOM tag | Notes | | ----------------------- | ---------------------------- | -------------------------------------------------------------------------- | | `study`, `series` | | References to the parent resources | | `sopClassUid` | `(0008,0016)` | | | `sopInstanceUid` | `(0008,0018)` | | | `instanceAvailability` | `(0008,0056)` | | | `timezoneOffsetFromUtc` | `(0008,0201)` | | | `instanceNumber` | `(0020,0013)` | Defaults to `"1"` when the source file omits it | | `rows`, `columns` | `(0028,0010)`, `(0028,0011)` | | | `bitsAllocated` | `(0028,0100)` | | | `numberOfFrames` | `(0028,0008)` | | | `metadata` | | The instance's full DICOM JSON dataset, serialized as a JSON string | | `raw` | | Reference to the `Binary` holding the original `.dcm` file | | `pixelData` | | References to per-frame pixel `Binary` resources, filled in asynchronously | ### What lands in `metadata` `DicomInstance.metadata` is the instance's DICOM JSON dataset — the same representation a WADO-RS metadata request returns — stored as a string. It is cleaned on the way in: - **File meta group `(0002,xxxx)`**, **`PixelData` `(7FE0,0010)`**, and **overlay data `(60xx,3000)`** are removed. Pixel data lives in `Binary` resources, not in the JSON. - **Binary-valued attributes** (value representations `OB`, `OD`, `OF`, `OL`, `OV`, `OW`, `UN`) up to **10 KB** are inlined as base64 under `InlineBinary`. Anything larger is dropped rather than bloating the resource. - **Sequences** are cleaned recursively, so the same rules apply at every nesting level. Only the **first 100 KB** of each uploaded file is parsed for metadata during the STOW-RS request. That is far more than a DICOM header needs and it keeps ingest streaming rather than buffering whole studies in memory. The complete file is always preserved in the `raw` `Binary` regardless. ### Pixel data extraction Storing an instance does not extract its pixels inline — that would make every `C-STORE` wait on decoding. Instead, the create is dispatched to a background worker: 1. A `DicomInstance` is created, or its `raw` reference changes. 2. The `dicom` worker enqueues a job on the `DicomQueue` BullMQ queue (three attempts, exponential backoff starting at one second). 3. The worker reads the raw `Binary`, parses the full file, and splits `PixelData` into frames. 4. Each frame is written as its own `Binary`, with `securityContext` set to the `DicomInstance` so it inherits the instance's access control. 5. `DicomInstance.pixelData` is patched with references to those binaries, in frame order. The `Binary.contentType` of each frame is derived from the file's Transfer Syntax UID: | Transfer Syntax UID | Content type | | -------------------------------------- | -------------------------- | | `1.2.840.10008.1.2.4.50`, `.57`, `.70` | `image/jpeg` | | `1.2.840.10008.1.2.4.90`, `.91` | `image/jp2` | | `1.2.840.10008.1.2.4.201`, `.202` | `image/jxl` | | Anything else, including uncompressed | `application/octet-stream` | Until the worker finishes, [frame retrieval](./dicomweb-api.md#wado-rs-retrieve-frames) returns `404` for that instance. Study and series queries work immediately. The worker is named `dicom` and can be enabled or disabled per server instance through the `workers.enabled` configuration setting, like any other Medplum worker. A deployment that runs workers on dedicated hosts needs `dicom` enabled on at least one of them, or pixel data is never extracted. ### Search parameters The DICOM resources are searchable through the normal [FHIR search API](/docs/search). | Resource | Search parameters | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DicomStudy` | `study-instance-uid`, `study-id`, `study-date`, `study-time`, `accession-number`, `modalities`, `referring-physicians-name`, `patient-name`, `patient-id` | | `DicomSeries` | `study`, `series-instance-uid`, `series-number`, `performed-procedure-step-start-date`, `performed-procedure-step-start-time`, `scheduled-procedure-step-id`, `requested-procedure-id` | | `DicomInstance` | `study`, `series`, `sop-class-uid`, `sop-instance-uid`, `instance-number` | ```ts // Every CT and MR study for an accession number const studies = await medplum.searchResources('DicomStudy', { 'accession-number': 'A12345', }); // Every series in a study const series = await medplum.searchResources('DicomSeries', { study: `DicomStudy/${studies[0].id}`, }); ``` Note that `modalities` is the only `token` parameter in the set; the rest of the study-level parameters are `string` searches, and `study-date` is a `date` search. ### Relationship to FHIR ImagingStudy Medplum does not currently create an [`ImagingStudy`](/docs/api/fhir/resources/imagingstudy) alongside a `DicomStudy`, and `DicomStudy.patientId` holds the DICOM Patient ID string rather than a reference to a Medplum [`Patient`](/docs/api/fhir/resources/patient). Linking imaging into the chart is on the roadmap. In the meantime, a [Bot](/docs/bots) subscribed to `DicomStudy` creation can do the reconciliation your workflow needs — matching `patientId` against your MRN identifier system, creating an `ImagingStudy` that references both the `Patient` and the study's UIDs, and flagging studies whose patient could not be resolved for manual review. Doing it in a Bot rather than at ingest keeps the matching logic — which is highly site-specific — under your control. --- ## DICOMweb API Medplum serves [DICOMweb](https://www.dicomstandard.org/using/dicomweb) under `/dicomweb` on the same host as the FHIR API. On Medplum's hosted service that is: ``` https://api.medplum.com/dicomweb ``` Every endpoint requires the same authentication as the rest of the Medplum API — an OAuth2 bearer token in the `Authorization` header — and resolves against the authenticated user's project and [access policy](/docs/access/access-policies). There is no separate DICOM credential to manage, and a user who cannot read a `DicomStudy` cannot retrieve it over DICOMweb either. CORS is enabled on the `/dicomweb/` prefix so browser-based viewers can call it directly. :::info[Beta] The implemented surface is the subset needed for storage and for a DICOMweb viewer to display a study. ::: ### Implemented endpoints | Service | Method | Path | | ----------- | ------ | ---------------------------------------------------------------------------------------- | | **STOW-RS** | `POST` | `/dicomweb/studies` | | **STOW-RS** | `POST` | `/dicomweb/studies/{studyUid}` | | **QIDO-RS** | `GET` | `/dicomweb/studies` | | **QIDO-RS** | `GET` | `/dicomweb/studies/{studyUid}/series` | | **WADO-RS** | `GET` | `/dicomweb/studies/{studyUid}/series/{seriesUid}/metadata` | | **WADO-RS** | `GET` | `/dicomweb/studies/{studyUid}/series/{seriesUid}/instances/{instanceUid}/frames/{frame}` | ### STOW-RS: Store instances ``` POST /dicomweb/studies POST /dicomweb/studies/{studyUid} Content-Type: multipart/related; type="application/dicom"; boundary={boundary} ``` The request body is a `multipart/related` body with one part per DICOM instance, each part carrying `Content-Type: application/dicom` and a complete DICOM Part 10 file. Both forms behave identically — the `{studyUid}` in the path is accepted for spec compatibility but the study is always determined from each instance's own Study Instance UID. Parts are processed as they stream in, so a multi-instance upload does not have to be buffered in memory. Each part is simultaneously written to a `Binary` and parsed for metadata. ```bash curl -X POST https://api.medplum.com/dicomweb/studies \ -H "Authorization: Bearer $MEDPLUM_TOKEN" \ -H 'Content-Type: multipart/related; type="application/dicom"; boundary=boundary' \ --data-binary @body.multipart ``` Constructing that multipart body by hand is tedious; the [Medplum CLI](./cli.md) and the [Agent](./agent-dimse.md) both do it for you. **Response `200`** — a DICOM JSON dataset containing a Referenced SOP Sequence `(0008,1199)` with one item per stored instance, each carrying the Referenced SOP Class UID `(0008,1150)`, Referenced SOP Instance UID `(0008,1155)`, and a Retrieve URL `(0008,1190)`. **Response `415`** — the `Content-Type` was not `multipart/related`. **Response `400`** — the multipart body could not be parsed, or an instance failed to store. :::caution The Retrieve URL returned in the response is currently a placeholder of the form `/instances/{id}/raw`, which is not a resolvable endpoint. Use the QIDO-RS and WADO-RS routes below to retrieve stored instances. ::: ### QIDO-RS: Search for studies ``` GET /dicomweb/studies Accept: application/dicom+json ``` Returns every `DicomStudy` visible to the caller as a DICOM JSON array, one dataset per study, with the study-level attributes described in the [data model](./data-model.md#dicomstudy). Query parameters such as `PatientName`, `AccessionNumber`, `ModalitiesInStudy`, `limit`, `offset`, `includefield`, and `fuzzymatching` are accepted by the route but are not applied — every study the caller can read is returned. For filtered or paginated queries, use the [FHIR search API](./data-model.md#search-parameters) against `DicomStudy`. ### QIDO-RS: Search for series ``` GET /dicomweb/studies/{studyUid}/series Accept: application/dicom+json ``` Returns the series in one study, as a DICOM JSON array. Each series dataset also repeats the study-level attributes, which is what viewers expect when building a study/series tree. **Response `404`** — no study with that Study Instance UID is visible to the caller. **Response `400`** — the study UID was missing or malformed. ### WADO-RS: Retrieve series metadata ``` GET /dicomweb/studies/{studyUid}/series/{seriesUid}/metadata Accept: application/dicom+json ``` Returns a DICOM JSON array with the full metadata dataset for every instance in the series — the cleaned dataset stored in [`DicomInstance.metadata`](./data-model.md#what-lands-in-metadata). This is the call a viewer makes to learn the geometry of a series before it starts pulling frames. **Response `404`** — the study or the series could not be found. ### WADO-RS: Retrieve frames ``` GET /dicomweb/studies/{studyUid}/series/{seriesUid}/instances/{instanceUid}/frames/{frame} ``` Returns one frame of pixel data as a `multipart/related` body. Frame numbers are **1-based**, as in the DICOM standard. The part's `Content-Type` reflects the encoding the frame was stored in — see [pixel data extraction](./data-model.md#pixel-data-extraction). | Status | Meaning | | ------ | -------------------------------------------------------------------------------------- | | `200` | Frame returned | | `400` | Missing or malformed study UID, series UID, instance UID, or a frame number below 1 | | `404` | Study, series, or instance not found — or pixel data has not been extracted for it yet | | `416` | The requested frame number exceeds the number of frames available for the instance | A `404` immediately after upload usually means the background worker has not finished extracting pixel data. Retry, or check that the `dicom` worker is running. Only a single frame number is supported per request. The standard's comma-separated frame list (`/frames/1,2,3`) is not implemented. ### See also - [DICOM Standard Part 18: Web Services](https://dicom.nema.org/medical/dicom/current/output/html/part18.html) - [STOW-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.5) · [WADO-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.4) · [QIDO-RS](https://dicom.nema.org/medical/dicom/current/output/html/part18.html#sect_10.6) --- ## DICOM & DICOMweb Medplum stores medical imaging alongside the rest of the patient record. A modality or PACS sends studies in over the DICOM network protocol or over DICOMweb, Medplum files them into [`DicomStudy`](/docs/api/fhir/medplum/dicomstudy), [`DicomSeries`](/docs/api/fhir/medplum/dicomseries), and [`DicomInstance`](/docs/api/fhir/medplum/dicominstance) resources, and a DICOMweb viewer such as [OHIF](https://ohif.org/) reads them back out — all under the same authentication, access policies, and audit trail as every other resource in the project. :::info[Beta] DICOM support is a [Beta](/docs/compliance/alpha-beta) feature — ready for testing, with a stable core contract. Tell us what you need at [hello@medplum.com](mailto:hello@medplum.com) or in a [GitHub issue](https://github.com/medplum/medplum/issues). ::: ### How it fits together ```mermaid flowchart LR M["Modality / PACS"] -- "DIMSE C-STORE" --> A["Medplum Agent"] A -- "HTTPS STOW-RS" --> S["Medplum Server"] C["Medplum CLI"] -- "HTTPS STOW-RS" --> S S --> R["DicomStudy
DicomSeries
DicomInstance
Binary (raw .dcm)"] R --> W["DICOM worker"] W --> P["Binary (pixel data,
one per frame)"] S -- "QIDO-RS / WADO-RS" --> V["OHIF Viewer"] ``` There are two ways in and one way out. **In, over the network protocol.** Imaging equipment overwhelmingly speaks DIMSE — the DICOM upper layer protocol over raw TCP — not HTTPS, and it speaks it on a hospital network that has no route to the public internet. The [Medplum Agent](./agent-dimse.md) runs inside that network, presents itself as a DICOM Storage SCP, accepts `C-STORE` from the modality, and forwards each instance to Medplum over an outbound HTTPS connection. This is the same Agent that handles HL7 v2 and ASTM traffic, so a site that already runs one gets imaging by adding a channel. **In, over HTTPS.** Anything that can speak DICOMweb — a cloud PACS, a research pipeline, an integration engine, or the [Medplum CLI](./cli.md) — can `POST` directly to the [STOW-RS endpoint](./dicomweb-api.md#stow-rs-store-instances). No Agent required. **Out, over HTTPS.** [QIDO-RS and WADO-RS](./dicomweb-api.md) serve the study list, series metadata, and pixel frames that a DICOMweb viewer needs. Medplum's implementation targets the request sequence the [OHIF Viewer](./ohif-viewer.md) makes. ### What gets stored A single DICOM instance arriving at Medplum produces four things: | Resource | Holds | | ------------------------------------------------------- | --------------------------------------------------------------------------------------- | | [`DicomStudy`](/docs/api/fhir/medplum/dicomstudy) | Study-level attributes — study UID, accession number, patient name and ID, study date | | [`DicomSeries`](/docs/api/fhir/medplum/dicomseries) | Series-level attributes — series UID, modality, series description | | [`DicomInstance`](/docs/api/fhir/medplum/dicominstance) | Instance-level attributes, plus the full DICOM JSON metadata and references to binaries | | [`Binary`](/docs/api/fhir/resources/binary) | The original, unmodified `.dcm` file | Studies and series are created conditionally on their DICOM UIDs, so the second instance of a series attaches to the study and series the first one created rather than duplicating them. A background worker then reads the raw file and extracts pixel data into one additional `Binary` per frame, which is what [WADO-RS frame retrieval](./dicomweb-api.md#wado-rs-retrieve-frames) serves. See the [Data Model](./data-model.md) for the full mapping from DICOM attributes to resource fields. Because these are ordinary Medplum resources, they are searchable with the standard FHIR search API, readable through the [TypeScript SDK](/docs/sdk/), subject to [access policies](/docs/access/access-policies), and able to trigger [Bots](/docs/bots) on create or update. ### Getting started 1. **Store a file from your laptop.** `medplum dicomweb stow MRBRAIN.DCM` uploads a DICOM file — or a whole directory of them — through STOW-RS with no infrastructure to set up. See the [CLI](./cli.md). 2. **Look at it.** Medplum's hosted cloud is preconfigured with an [OHIF Viewer](./ohif-viewer.md) at [viewer.medplum.com](https://viewer.medplum.com) — sign in and the study is there. 3. **Connect a modality.** Add a DICOM channel to a [Medplum Agent](./agent-dimse.md) and send a `C-ECHO`, then a `C-STORE`, from the device. ### Reference - [Data Model](./data-model.md) — resource types, search parameters, and DICOM attribute mapping - [DICOMweb API](./dicomweb-api.md) — the implemented HTTP endpoints - [Medplum Agent](./agent-dimse.md) — DIMSE `C-STORE` and `C-ECHO` from inside the firewall - [Medplum CLI](./cli.md) — `medplum dicomweb stow` - [OHIF Viewer](./ohif-viewer.md) — viewer configuration - [DICOM Standard Part 18: Web Services](https://dicom.nema.org/medical/dicom/current/output/html/part18.html) --- ## OHIF Viewer [OHIF](https://ohif.org/) is an open source, zero-footprint DICOM viewer that reads studies over DICOMweb. Medplum's DICOMweb implementation targets the request sequence OHIF makes, so a study stored in Medplum can be displayed without a gateway or a separate image server in between. ### Medplum hosted cloud Medplum's hosted cloud is preconfigured with an OHIF instance at **[viewer.medplum.com](https://viewer.medplum.com)**. There is nothing to deploy and nothing to configure — sign in with your Medplum account and the studies in your project are there. ![An MR brain study stored in Medplum, displayed in the OHIF Viewer at viewer.medplum.com](/img/blog/2026-08-dicom-ohif-viewer.webp) The viewer authenticates against Medplum with OAuth2 and reads over the same [DICOMweb API](./dicomweb-api.md) documented here, so what you see in it is exactly what any DICOMweb client would get. ### Self-hosted viewer deployments Running your own OHIF deployment against a self-hosted Medplum server is an enterprise feature. Contact us at [hello@medplum.com](mailto:hello@medplum.com) to talk through it. ### How OHIF uses the API | OHIF screen | Request | Status in Medplum | | --------------- | --------------------------------------------------------------------------------------------- | ----------------------------- | | Study list | `GET /dicomweb/studies?limit=101&offset=0&fuzzymatching=false&includefield=00081030,00080060` | Works; parameters are ignored | | Study → series | `GET /dicomweb/studies/{studyUid}/series` | Works | | Series metadata | `GET /dicomweb/studies/{studyUid}/series/{seriesUid}/metadata` | Works | | Image display | `GET /dicomweb/studies/{studyUid}/series/{seriesUid}/instances/{instanceUid}/frames/{frame}` | Works, one frame per request | Image retrieval goes through **WADO-RS**, not WADO-URI — Medplum implements frame retrieval and does not implement WADO-URI at all. Any DICOMweb viewer pointed at Medplum needs to be configured the same way. ### Authentication and access control The `/dicomweb` endpoints use the same OAuth2 bearer tokens as the rest of the Medplum API, and resolve against the authenticated user's project and [access policy](/docs/access/access-policies). A user who cannot read a `DicomStudy` cannot retrieve it in the viewer either — imaging inherits the project's existing authorization rather than introducing a second model to keep in sync. CORS is enabled on the `/dicomweb/` prefix, so a viewer served from a different origin can call the API directly from the browser. ### FHIRcast OHIF hardcodes its FHIRcast hub URL to `/api/hub`. Medplum aliases that path to the latest FHIRcast version it supports, so OHIF's context synchronization works against a Medplum server without reconfiguration. See [FHIRcast](/docs/fhircast) for what context synchronization enables — driving the viewer's displayed study from a clinician's selection elsewhere in the application. ### See also - [OHIF Viewer documentation](https://docs.ohif.org/) - [DICOMweb API](./dicomweb-api.md) --- ## FHIR Basics import MedicationExample from '!!raw-loader!@site/..//examples/src/medications/medication-codes.ts'; import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; [patient]: /docs/api/fhir/resources/patient [practitioner]: /docs/api/fhir/resources/practitioner [medicationrequest]: /docs/api/fhir/resources/medicationrequest [medication]: /docs/api/fhir/resources/medication [device]: /docs/api/fhir/resources/device [procedure]: /docs/api/fhir/resources/procedure [careplan]: /docs/api/fhir/resources/careplan [encounter]: /docs/api/fhir/resources/encounter [reference]: /docs/api/fhir/datatypes/reference [codeableconcept]: /docs/api/fhir/datatypes/codeableconcept To follow along with the concepts described in this guide, you can download and import this [sample FHIR bundle](https://drive.google.com/file/d/1196wfX-aBMUK33GdQiIdBUNlYn_cymBU/view?usp=drive_link) into your Medplum project. For instructions on how to import the data, refer to our [Import Sample Data](/docs/tutorials/importing-sample-data) guide. ### Why FHIR? Medplum stores healthcare data using the FHIR standard. Storing data according to this standard provides developers with the following benefits: - **Interoperability**: Increasingly, healthcare partners are exposing their data via FHIR APIs. Storing your data according to FHIR spec smooths the path to interoperating with multiple partners - **Future Proofing**: The healthcare ecosystem is complex and fragmented. As they encounter these complexities, many digital health companies end up performing costly data migrations. The FHIR spec anticipates many of the complexities that arise in the healthcare domain, helping teams avoid these backend rewrites. While FHIR is quite powerful, it can have a bit of a learning curve. This page covers the basic concepts for understanding FHIR data in Medplum. For more information, you can check out the [official FHIR documentation](http://hl7.org/fhir/). ### Storing Data: Resources A core data object in FHIR is called a [**Resource**](https://www.hl7.org/fhir/resource.html). You can think of Resources as **objects** in object oriented languages. The FHIR standard defines over 150 [**Resource Types**](./api/fhir/resources) that model a broad range of healthcare-specific concepts. These include **concrete entities** ([`Patient`][patient], [`Medication`][medication], [`Device`][device]) as well as **abstract concepts** ([`Procedure`][procedure], [`CarePlan`][careplan], [`Encounter`][encounter]). Each field in a resource is called an [**Element**](https://hl7.org/fhir/R4/element.html), each of which can be a **primitive type** (e.g. `string`, `number`, `date`) or a **complex type** (e.g. [`HumanName`](/docs/api/fhir/datatypes/humanname)). Lastly, all resources have an `id` element, which is a server-assigned identifier that serves as their **primary key**.
Example [`Patient`][patient] The example below shows an example [`Patient`][patient] resource. Here we can see that the [`Patient`][patient] contains multiple elements, including `name`, `telecom`, and `address`. ```javascript { // Resource Type (i.e. "class name") "resourceType": "Patient", // Unique id for this resource "id": "j_chalmers", // Patient Name (could have multiple) "name": [ { "use": "official", "family": "Chalmers", "given": ["Peter", "James"] }, { "use": "usual", "family": "Chalmers", "given": ["Jim"] } ], // Phone + email info "telecom": [ { "system": "phone", "value": "(03) 3410 5613", "use": "mobile" } ], // Address (could have multiple) "address": [ { "use": "home", // 'home', 'office', etc. "line": ["534 Erewhon St"], "city": "PleasantVille", "district": "Rainbow", "state": "Vic", "postalCode": "3999", // Single string version of address, used for display "text": "534 Erewhon St PeasantVille, Rainbow, Vic 3999" } ] } ```
### Linking Data: References When working with FHIR, clinical data is often split across multiple resources. For example a prescription is related to the receiving patient, and a diagnostic report may consist of multiple observations. To create a link between objects, we use [`Reference`][reference] elements. A FHIR [`Reference`][reference] is an element that functions like a **foreign key** in traditional relational databases to create 1-to-1 or many-to-many relationships between resources. [`Reference`][reference] elements have the following structure: ```ts { "reference" : ":resourceType/:id", // Resource type + unique id of the referenced Resource "display" : string, // Display string for the reference "type" : uri, // Resource type (if using a "logical reference") "identifier" : Identifier } ``` In Medplum, we will typically only use the `reference` and `display` elements.
Example: Linking a [`MedicationRequest`][medicationrequest] to a [`Patient`][patient] and [`Practitioner`][practitioner] The example below shows a resource modeling a prescription (i.e. [`MedicationRequest`][medicationrequest]) with two references: **`subject` (i.e. the patient)** and **`requester` (i.e. the requesting physician)**. ```javascript { "resourceType": "MedicationRequest", "id": "medrx002", // Reference to the patient for whom medication is being ordered "subject": { "reference": "Patient/pat1", "display": "Donald Duck" }, "dosageInstruction": [ { "text": "Take one tablet daily as directed" } ], // Reference to the requesting physician "requester": { "reference": "Practitioner/f007", "display": "Patrick Pump" } } ```
### Querying Data: Search FHIR offers both a [REST API](/docs/search) and [GraphQL API](/docs/graphql) to query, search, sort, and filter resources by specific criteria (see [this blog post](/blog/graphql-vs-rest) for tradeoffs between REST and GraphQL). **FHIR resources cannot be searched by arbitrary fields**. Instead, the specification defines specific [search parameters](/docs/search/basic-search#search-parameters) for each resource that can be used for queries. Refer to the [Medplum search documentation](/docs/search/basic-search) for a more in-depth tutorial on FHIR search. ### Standardizing Data: Codeable Concepts The healthcare system commonly uses standardized coding systems to share information between organizations about **diagnoses**, **procedures**, **clinical outcomes**, and **billing**. See our summary on [Common Terminologies](/docs/terminology/common-terminologies) for an overview of the most frequently used codes in healthcare. Because there are multiple code systems for many domains, the same _concept_ can be defined in _multiple code systems_. To handle this mapping from concept to system, the FHIR defines the [`CodeableConcept`][codeableconcept] element type. A [`CodeableConcept`][codeableconcept] consists of two parts: - A `text` element - describes the concept in plain language - A `coding` element - an array of `(system, code)` pairs that provide the standard code for the concept within each code system. FHIR [`CodeableConcepts`][codeableconcept] use the `system` element to identify each code system within the `coding` array. By convention, FHIR uses absolute URLs to enforce that these systems are a globally unique namespace. _However, these URLs do not always point to hosted web sites._ More detailed information about using coded values with FHIR are available in our [Terminology Services documentation](/docs/terminology). Refer to [this blog post](/blog/demystifying-fhir-systems) for a longer discussion of `system` strings. Refer to the [FHIR official documentation](https://hl7.org/fhir/R4/terminologies-systems.html) for a list of `systems` for common healthcare code systems.
Example: Tylenol Below is an example [`CodeableConcept`][codeableconcept], that defines the medication Tylenol, in both the [RXNorm](/docs/medications/medication-codes#rxnorm) or [NDC](/docs/medications/medication-codes#ndc) systems. {MedicationExample}
### Naming Data: Identifiers One issue in healthcare applications is that the same entity can have many different identifiers in different systems. For example, a patient might be identified simultaneously by their: - Social Security Number (SSN) - Medical Record Number (MRN) - Medicare Beneficiary Identifier - Driver's License Number FHIR anticipates this complexity by allowing each resource to have multiple identifiers. Each identifier is defined by a `(system, value)` pair. As with [`CodeableConcepts`][codeableconcept], the `system` acts as namespace for the identifier, and _must be specified as an absolute URL_ to ensure that it is globally unique. Refer to [this blog post](/blog/demystifying-fhir-systems#identifiers-1) for best practices on using identifier `system` strings. Using the identifier system allows you to simplify your healthcare applications by consolidating data in a single resource, while allowing different systems to access the data by different ID schemes. When generating MRNs for new patients, store the value in `Patient.identifier` with a stable `system` URL for the assigning authority. You can use an MRN from a source system during migration, use the Medplum `Patient.id` as the durable patient number for new applications, or assign an MRN from a bot or backend workflow when the `Patient` is created. If you generate MRNs automatically, make the workflow idempotent: if the patient already has an identifier for your MRN system, do not create a second one.
Example: [`Patient`][patient] with two medical record numbers (MRNs) The example `Patient` below has three identifiers: **an SSN and two MRN identifiers** from different hospital systems. ```javascript { // Resource Type (i.e. "class name") "resourceType": "Patient", // Unique id for this resource "id": "j_chalmers", // Patient Name (could have multiple) "name": [ { "use": "official", "family": "Chalmers", "given": ["Peter", "James"] } ], "identifier": [ // Social Security Number ID (US-SSN) { "system": "http://hl7.org/fhir/sid/us-ssn", "value": "011-11-1234" }, // MRN - Hospital 1 { "system": "http://hospital-1.org", "value": "MRN-12345678" }, // MRN - Hospital 2 { "system": "http://hospital-2.org", "value": "0987AZ6" } ] } ```
### ValueSets ValueSets are a collection of codes that are used to represent a concept. They act as filters or subsets of larger terminology systems (like SNOMED CT, LOINC, or ICD-10) to specify exactly which codes are appropriate for a specific use case. They are defined by a `system` and a `concept` array. The `concept` array contains the codes that are part of the ValueSet. ```json { "resourceType": "ValueSet", "url": "http://example.com/ValueSet/vitals", "name": "vitals", "title": "Vital Signs", "status": "active", "compose": { "include": [ { "system": "http://loinc.org", "concept": [ { "code": "8310-5", "display": "Body temperature" }, { "code": "8462-4", "display": "Diastolic blood pressure" }, { "code": "8480-6", "display": "Systolic blood pressure" }, { "code": "8867-4", "display": "Heart rate" }, { "code": "9279-1", "display": "Respiratory rate" } ] } ] } } ``` ### Listening for changes: Subscriptions **FHIR has a built-in [Subscription](./api/fhir/resources/subscription) resource** that is used to define a push-based subscription to resources in the system, analogous to web-hooks. A `Subscription` has two primary elements: - **criteria**: This is a string expression that defines _which_ resources to listen to, specified in [FHIRPath](https://hl7.org/fhirpath/) format. This subscription is invoked whenever a resource that matches the criteria is created or updated. - **channel**: this describes the kind of action that the `Subscription` will take when it sees a matching resource. Currently, the possible values are `rest-hook`, `websocket`, `email`, and `message`. In Medplum, a powerful feature is to **use a [Medplum Bot](./bots)** as the endpoint of the `rest-hook` channel. This allows you to run an arbitrary piece of code in response to data changes and automate your medical workflows. See our [Bot-Subscription tutorial](./bots/bot-for-questionnaire-response) for more information. --- ## Binary Data import MedplumCodeBlock from '@site/src/components/MedplumCodeBlock'; import BrowserOnlyTabs from '@site/src/components/BrowserOnlyTabs'; import TabItem from '@theme/TabItem'; import ExampleCode from '!!raw-loader!@site/..//examples/src/fhir-datastore/binary-data.ts'; Managing, storing, and retrieving binary files is a fundamental requirement in modern healthcare applications. Medplum, an open-source, API-first healthcare technology platform, provides a structured approach to this. This article dives into how to upload binary files to Medplum using the FHIR `Binary` resource type, detailing the process and Medplum's specific optimizations. ### Introduction to FHIR `Binary` The FHIR (Fast Healthcare Interoperability Resources) standard introduces the `Binary` resource type. It's designed to hold any kind of data in a binary form, encompassing but not limited to, images, PDFs, audio, and even video. This allows a standardized means to store and reference raw content, simplifying integration between healthcare systems. ### Creating a FHIR `Binary` To upload binary files to Medplum, you'll first need to create a `Binary` resource. #### Using raw HTTP via `curl` The HTTP API to upload a `Binary` is straightforward: ```bash curl -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: image/jpeg" \ --data-binary "@./yourfile.jpg" \ https://api.medplum.com/fhir/R4/Binary ``` This is a short snippet, but each line has significant meaning. First, the `Authorization` header is required to authenticate the request. See the [**Client Credentials tutorial**](/docs/auth/client-credentials) guide for how to obtain an access token. Next, the `Content-Type` header is required to specify the MIME type of the file being uploaded. You can always use the generic `application/octet-stream` type if you don't know the exact type, but beware that this may cause issues when consuming the `Binary` in an application. It is always recommended to use the correct MIME type if possible. See [Common MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types). To upload a file, use the `--data-binary` flag. The `--data-binary` flag is used to upload the raw binary data to avoid any encoding issues. Also note `curl`'s special syntax for referencing a file by file name using the "@" character. #### Using the Medplum SDK `MedplumClient` Medplum provides a JavaScript/TypeScript SDK that makes it easy to upload binary files. {ExampleCode} The return value is the newly created resource, including the ID and meta. The `data` parameter can be `string | File | Blob | Uint8Array`. A `File` object often comes from a `` element. A `Blob` object often comes from a `fetch()` call. A `Uint8Array` object often comes from a `FileReader` or `ArrayBuffer`. Beware passing in a `string`! This will cause the string to be encoded as UTF-8, which may not be what you want. If you have base-64 encoded content, you must first decode it to one of the binary types first before passing it to `createBinary()`. In addition to `medplum.createBinary()`, the Medplum SDK also provides `medplum.createAttachment()` which is similar but also creates an `Attachment` object. Note that `Attachment` is not a resource type, it is merely an in memory object to make it easier to work with binary data. #### Create `Binary` via external URL For large files such as videos and images, it can be inconvenient to download contents to the client before uploading to Medplum. In these situations, you can create a [`Media`](../api/fhir/resources/media) resource with a `url` parameter pointing to the location of the content. **TypeScript** {ExampleCode} **Python** ```py API_URL = 'https://api.medplum.com/fhir/R4' MEDIA_URL = 'https://images.unsplash.com/photo-1581385339821-5b358673a883' media = { 'resourceType': 'Media', 'basedOn': [{ 'reference': 'ServiceRequest/12345' }], 'status': 'completed', # `status` is a required field 'content': { 'title': 'plums-python.jpg', 'contentType': 'image/jpeg', 'url': MEDIA_URL, } }; requests.post(f'{API_URL}/Media', json=media, headers={ 'Authorization': f'Bearer {auth_token}' }) ``` See the [**Client Credentials tutorial**](/docs/auth/client-credentials) guide for how to obtain an access token ### Referencing a `Binary` in an Attachment Once uploaded, the `Binary` resource can be referenced in various FHIR resources. **a. Patient Profile Picture - `Patient.photo`:** {ExampleCode} **b. Message Attachment - `Communication.payload.contentAttachment`:** {ExampleCode} :::caution[Attachment Data] When creating an [`Attachment`](/docs/api/fhir/datatypes/attachment), we recommend that you _do not_ use the `data` field to provide the full binary data string. This can make the attachment very large and can cause issues when making HTTP requests. See [External files and Document references](/docs/fhir-datastore/external-documents#example-creating-a-document-reference-for-a-binary-file) for an example of how to upload and reference [`Binary`](/docs/api/fhir/resources/binary) resources. ::: ### Consuming a FHIR `Binary` in an Application In a normal FHIR server, when reading a FHIR resource that references a `Binary`, you'd receive a URL to the `Binary` resource. For example: ```json { "resourceType": "Patient", "photo": [ { "contentType": "image/jpeg", "url": "Binary/12345" } ] } ``` And then you would be responsible for downloading the `Binary` resource. Unfortunately, this poses of challenges: 1. Accessing content in a browser can be tricky. Elements like `` or `