Skip to main content

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:

{
"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 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.
// 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. 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.

{
"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).

{
"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.

{
"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.

{
"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.

{
"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.

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.
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 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 TypeGenerated Resources
QuestionnaireTask with Questionnaire input reference
ActivityDefinitionTask + ServiceRequest (if kind is ServiceRequest)
NoneTask 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:

{
"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.

{
"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" }
]
}
]
}