Skip to main content

Medication Plan with Intake Reminders

Use case

A chronic care management app needs to send patients timely reminders to take their medications. Dr. Chen is treating Maria, a patient with hypertension, and prescribes Lisinopril 10mg every morning and Amlodipine 5mg every evening. The app should receive a notification at 08:00 and 20:00 each day so it can send a push notification to Maria's phone. When Maria confirms she has taken her dose, the task is marked as completed. If she misses a dose, the app shows it as overdue.

Other scenarios this recipe applies to

  • Post-discharge wound care: A hospital sends reminders for wound care activities (clean wound, change dressing) at specific intervals after discharge. A home nurse verifies completion.
  • Directly Observed Therapy (DOT): A tuberculosis treatment program schedules medication intake observations. A healthcare worker must verify intake and record completion in real time.

Why start from a CarePlan, not a PlanDefinition?

Medication plans are inherently individual. Unlike a screening protocol that applies identically to every patient, a medication regimen is tailored to the specific patient: which drugs, which doses, which times, adjusted for their condition, co-medications, allergies, and tolerance. No two hypertension patients take exactly the same combination.

A PlanDefinition is a reusable template designed for standardized care. Using it here would mean creating a template, applying it with $apply, and then customizing the result - which is more complexity than simply creating the CarePlan directly.

A CarePlan is patient-specific by design. The practitioner creates a CarePlan, adds MedicationRequest resources with dosage timing, and enables scheduling. This maps naturally to the clinical workflow: a doctor writes prescriptions and a care plan coordinates adherence.

When would PlanDefinition make sense for medications?

Protocol-driven regimens like chemotherapy cycles, where the same drug sequence at the same doses applies to many patients, are a good fit for PlanDefinition. For everyday medication management where each patient's regimen is unique, starting directly with a CarePlan is simpler. See the Patient Surveillance recipe for a PlanDefinition-based approach.

What you will build

Prerequisites

  • Fire Arrow Server is running with CarePlan Events enabled.
  • Subscriptions are enabled (hapi.fhir.subscription.resthook_enabled: true).
  • Authentication is configured. See Authentication.
  • Your app has a publicly accessible webhook endpoint (or use polling as an alternative).

Choosing the right FHIR resources

MedicationRequest: the prescription

A MedicationRequest represents a single prescription. It carries the medication name, dosage, and - critically - the timing schedule in dosageInstruction[].timing. Fire Arrow Server's CarePlan expander reads this timing to determine when to generate Task resources.

For Maria's regimen, you need two MedicationRequests: one for Lisinopril (morning) and one for Amlodipine (evening).

CarePlan: the coordination container

A CarePlan ties the MedicationRequests together and opts into automatic scheduling. Its activity[].reference fields point to the MedicationRequests. The scheduling meta tag tells Fire Arrow Server to start materializing Tasks from the dosage timing.

Task: a single dose occasion

Task resources are generated automatically by the server. Each Task represents a single dose occasion - "take Lisinopril at 08:00 on April 5." Tasks transition through statuses:

  1. requested - created by the server, not yet due.
  2. ready - the dose is now due. This transition triggers the webhook notification.
  3. completed - the patient confirmed intake.

The server creates Tasks within a rolling time horizon (e.g., 14 days ahead) and continuously generates new ones as time passes.

Subscription: the notification mechanism

A FHIR Subscription watches for Task status changes and sends a webhook to your app. Fire Arrow Server's $subscribe-due-events operation creates a properly configured Subscription for you.

Securing access

Medication data is sensitive clinical information. The authorization rules must address several concerns:

  1. Patients should see their own CarePlan, MedicationRequests, and Tasks - but only their own. They need update access to Tasks so they can mark doses as completed.
  2. Practitioners should be able to create and modify CarePlans and MedicationRequests for patients in their organization, and read Tasks to monitor adherence.
  3. Webhook payloads sent by Fire Arrow Server are intentionally minimal - they contain only the Task ID and CarePlan reference, no patient-identifiable information. Your app must make an authenticated request to fetch the full Task details. This protects PHI even if the webhook channel is compromised.
  4. Subscription creation requires its own authorization rule.

Complete authorization rules

fire-arrow:
authorization:
default-validator: Forbidden
validation-rules:
# --- Patient rules ---

- client-role: Patient
resource: Patient
operation: read
validator: PatientCompartment
- client-role: Patient
resource: Patient
operation: me
validator: PatientCompartment

# Patients read their own medication plan
- client-role: Patient
resource: CarePlan
operation: read
validator: PatientCompartment
- client-role: Patient
resource: CarePlan
operation: search
validator: PatientCompartment
- client-role: Patient
resource: MedicationRequest
operation: read
validator: PatientCompartment
- client-role: Patient
resource: MedicationRequest
operation: search
validator: PatientCompartment

# Patients read and complete Tasks (update needed for marking complete)
- client-role: Patient
resource: Task
operation: read
validator: PatientCompartment
- client-role: Patient
resource: Task
operation: search
validator: PatientCompartment
- client-role: Patient
resource: Task
operation: update
validator: PatientCompartment

# --- Practitioner rules ---

- client-role: Practitioner
resource: Practitioner
operation: me
validator: Allowed

- client-role: Practitioner
resource: Patient
operation: read
validator: LegitimateInterest
- client-role: Practitioner
resource: Patient
operation: search
validator: LegitimateInterest

# Practitioners manage medication plans
- client-role: Practitioner
resource: CarePlan
operation: read
validator: LegitimateInterest
- client-role: Practitioner
resource: CarePlan
operation: search
validator: LegitimateInterest
- client-role: Practitioner
resource: CarePlan
operation: create
validator: LegitimateInterest
- client-role: Practitioner
resource: CarePlan
operation: update
validator: LegitimateInterest

- client-role: Practitioner
resource: MedicationRequest
operation: read
validator: LegitimateInterest
- client-role: Practitioner
resource: MedicationRequest
operation: search
validator: LegitimateInterest
- client-role: Practitioner
resource: MedicationRequest
operation: create
validator: LegitimateInterest
- client-role: Practitioner
resource: MedicationRequest
operation: update
validator: LegitimateInterest

# Practitioners monitor task completion
- client-role: Practitioner
resource: Task
operation: read
validator: LegitimateInterest
- client-role: Practitioner
resource: Task
operation: search
validator: LegitimateInterest

# Practitioners subscribe to notifications
- client-role: Practitioner
resource: Subscription
operation: subscribe
validator: LegitimateInterest

Step-by-step instructions

Step 1: Create MedicationRequest resources

Create a MedicationRequest for each medication in the patient's regimen. The key is the dosageInstruction[].timing field - this is what Fire Arrow Server's CarePlan expander reads to generate tasks.

Lisinopril 10mg - once daily at 08:00:

curl -X POST http://localhost:8080/fhir/MedicationRequest \
-H "Content-Type: application/fhir+json" \
-H "Authorization: Bearer <practitioner-token>" \
-d '{
"resourceType": "MedicationRequest",
"status": "active",
"intent": "order",
"medicationCodeableConcept": {
"coding": [{
"system": "http://www.nlm.nih.gov/research/umls/rxnorm",
"code": "314076",
"display": "Lisinopril 10 MG Oral Tablet"
}],
"text": "Lisinopril 10mg"
},
"subject": {
"reference": "Patient/maria"
},
"authoredOn": "2026-04-04",
"requester": {
"reference": "Practitioner/dr-chen"
},
"dosageInstruction": [{
"text": "Take one tablet every morning at 08:00",
"timing": {
"repeat": {
"frequency": 1,
"period": 1,
"periodUnit": "d",
"timeOfDay": ["08:00:00"]
}
},
"doseAndRate": [{
"doseQuantity": {
"value": 10,
"unit": "mg",
"system": "http://unitsofmeasure.org",
"code": "mg"
}
}]
}]
}'

Amlodipine 5mg - once daily at 20:00:

curl -X POST http://localhost:8080/fhir/MedicationRequest \
-H "Content-Type: application/fhir+json" \
-H "Authorization: Bearer <practitioner-token>" \
-d '{
"resourceType": "MedicationRequest",
"status": "active",
"intent": "order",
"medicationCodeableConcept": {
"coding": [{
"system": "http://www.nlm.nih.gov/research/umls/rxnorm",
"code": "329528",
"display": "Amlodipine 5 MG Oral Tablet"
}],
"text": "Amlodipine 5mg"
},
"subject": {
"reference": "Patient/maria"
},
"authoredOn": "2026-04-04",
"requester": {
"reference": "Practitioner/dr-chen"
},
"dosageInstruction": [{
"text": "Take one tablet every evening at 20:00",
"timing": {
"repeat": {
"frequency": 1,
"period": 1,
"periodUnit": "d",
"timeOfDay": ["20:00:00"]
}
},
"doseAndRate": [{
"doseQuantity": {
"value": 5,
"unit": "mg",
"system": "http://unitsofmeasure.org",
"code": "mg"
}
}]
}]
}'

Save the returned IDs (e.g., MedicationRequest/med-lisinopril, MedicationRequest/med-amlodipine).

In the Web UI, you can create these via the Resource Browser by selecting MedicationRequest and using the JSON editor.

Step 2: Create a CarePlan referencing the MedicationRequests

Create a CarePlan that ties the medications together and opts into automatic scheduling. The scheduling meta tag (https://firearrow.io/fhir/careplan-scheduling with code scheduled) tells Fire Arrow Server to start materializing Tasks from the dosage timing.

curl -X POST http://localhost:8080/fhir/CarePlan \
-H "Content-Type: application/fhir+json" \
-H "Authorization: Bearer <practitioner-token>" \
-d '{
"resourceType": "CarePlan",
"meta": {
"tag": [{
"system": "https://firearrow.io/fhir/careplan-scheduling",
"code": "scheduled"
}]
},
"status": "active",
"intent": "plan",
"title": "Hypertension Medication Plan - Maria",
"description": "Daily Lisinopril (morning) and Amlodipine (evening) for blood pressure management.",
"subject": {
"reference": "Patient/maria"
},
"period": {
"start": "2026-04-04",
"end": "2026-07-04"
},
"author": {
"reference": "Practitioner/dr-chen"
},
"activity": [
{
"reference": {
"reference": "MedicationRequest/med-lisinopril"
}
},
{
"reference": {
"reference": "MedicationRequest/med-amlodipine"
}
}
]
}'

Key points:

  • meta.tag with scheduled - this is what triggers the materialization engine. Without this tag, the CarePlan is just data; with it, Fire Arrow Server actively creates Task resources based on the dosage timing.
  • activity[].reference - each activity points to a MedicationRequest. The server's CarePlan expander reads the dosageInstruction[].timing from these referenced resources to determine when to create tasks.
  • period - defines the overall duration of the medication plan. Tasks are only materialized within this period.

In the Web UI, open Care Plans, click + New Care Plan, set the metadata, and add activities by referencing the MedicationRequest resources.

Step 3: Enable CarePlan Events on the server

Configure the CarePlan Events system in your application.yaml:

fire-arrow:
careplan-events:
enabled: true
scheduling:
horizon-duration: "P14D"
max-occurrences-per-activity: 30
subscriptions:
max-ttl: "P90D"
delivery:
max-consecutive-failures: 5
task:
auto-transition-to-ready: true
SettingRecommended valueWhy
horizon-durationP14D (14 days)For twice-daily medications, a 14-day horizon creates about 28 tasks per medication - enough to cover two weeks ahead without generating thousands of tasks.
max-occurrences-per-activity30Safety limit per activity. Two medications x once daily x 14 days = 28 tasks per activity, so 30 provides a small buffer.
max-ttlP90D (90 days)Maximum lifetime for a subscription. For a 3-month medication plan, this matches the plan duration. Renew the subscription before it expires.
max-consecutive-failures5If the webhook endpoint is unreachable 5 times in a row, the subscription is disabled. Set this based on your app's expected uptime.
auto-transition-to-readytrueAutomatically move tasks from requested to ready when their due time arrives. This is what triggers the webhook.

For the full configuration reference, see CarePlan Events.

Step 4: Subscribe to due events

You have three options for receiving notifications when medication tasks become due.

Option A: Use $subscribe-due-events (recommended)

This convenience operation creates a properly configured Subscription in one call:

curl -X POST http://localhost:8080/fhir/CarePlan/careplan-maria/\$subscribe-due-events \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <practitioner-token>" \
-d '{
"resourceType": "Parameters",
"parameter": [
{ "name": "endpoint", "valueUrl": "https://your-app.example.com/webhooks/medication-reminders" }
]
}'

Option B: Use the Web UI

Open the CarePlan in the Care Plan editor, click Materialize in the toolbar, enter your webhook URL, and confirm.

Option C: Use polling instead of webhooks

If your app cannot receive webhooks (e.g., a mobile app without a server component), skip the subscription and poll for ready tasks:

curl "http://localhost:8080/fhir/Task?status=ready&based-on=CarePlan/careplan-maria" \
-H "Authorization: Bearer <patient-token>"

This returns all tasks that are currently due. Poll at a suitable interval (e.g., every 5 minutes).

Step 5: Receive and handle webhook notifications

When a medication task becomes due, Fire Arrow Server sends a minimal notification to your webhook endpoint:

{
"resourceType": "Task",
"id": "task-abc123",
"status": "ready",
"basedOn": [{ "reference": "CarePlan/careplan-maria" }]
}

The payload is intentionally minimal - it does not contain the patient's name, medication details, or any Protected Health Information. This protects patient privacy even if the webhook channel is compromised or logged by intermediate proxies.

Your app must fetch the full Task details with an authenticated request, then send the push notification. Here is a complete Python example:

from flask import Flask, request, jsonify
import requests

app = Flask(__name__)

FIRE_ARROW_BASE = "http://localhost:8080/fhir"
SERVICE_TOKEN = "Bearer <service-account-token>"

@app.route("/webhooks/medication-reminders", methods=["POST"])
def handle_medication_reminder():
notification = request.get_json()
task_id = notification["id"]

# Fetch the full Task with authentication
task_response = requests.get(
f"{FIRE_ARROW_BASE}/Task/{task_id}",
headers={"Authorization": SERVICE_TOKEN}
)
task = task_response.json()

# Extract the medication reference from the Task
careplan_ref = task["basedOn"][0]["reference"]
patient_ref = task["for"]["reference"]
execution_start = task["executionPeriod"]["start"]

# Fetch the CarePlan to find which medication this task is for
careplan = requests.get(
f"{FIRE_ARROW_BASE}/{careplan_ref}",
headers={"Authorization": SERVICE_TOKEN}
).json()

# Send push notification to the patient
send_push_notification(
patient_ref=patient_ref,
message=f"Time to take your medication! Due at {execution_start}",
task_id=task_id
)

return jsonify({"status": "ok"}), 200

def send_push_notification(patient_ref, message, task_id):
# Replace with your push notification service (Firebase, APNs, etc.)
print(f"Push to {patient_ref}: {message} (task: {task_id})")

Step 6: Patient confirms intake

When the patient taps "I took my medication" in the app, update the Task status to completed:

curl -X PUT http://localhost:8080/fhir/Task/task-abc123 \
-H "Content-Type: application/fhir+json" \
-H "Authorization: Bearer <patient-token>" \
-d '{
"resourceType": "Task",
"id": "task-abc123",
"status": "completed",
"basedOn": [{ "reference": "CarePlan/careplan-maria" }],
"for": { "reference": "Patient/maria" },
"executionPeriod": {
"start": "2026-04-05T08:00:00Z",
"end": "2026-04-05T08:05:00Z"
}
}'

The server's activity completion service may update the corresponding CarePlan activity status to reflect overall progress.

Step 7: Handle missed doses

Tasks that are not completed remain in ready status. Your app can query for overdue tasks to show "missed" indicators:

# Find all overdue tasks (status=ready, sorted by date)
curl "http://localhost:8080/fhir/Task?status=ready&based-on=CarePlan/careplan-maria&_sort=date" \
-H "Authorization: Bearer <patient-token>"

If a task has been in ready status for longer than expected (e.g., more than 2 hours past the scheduled time), the app can display it as "missed" in the patient's medication log.

For practitioners monitoring adherence across many patients:

# Find all overdue tasks in the organization
curl "http://localhost:8080/fhir/Task?status=ready&_sort=date" \
-H "Authorization: Bearer <practitioner-token>"

The LegitimateInterest validator automatically narrows results to patients in the practitioner's organization.

Step 8: Renewing and stopping

Renew the subscription before it expires (the max-ttl is 90 days by default):

curl -X POST http://localhost:8080/fhir/CarePlan/careplan-maria/\$renew-due-events \
-H "Authorization: Bearer <practitioner-token>"

Stop scheduling when the medication plan ends or is discontinued:

# Option A: Unsubscribe (stops webhooks but keeps the CarePlan)
curl -X POST http://localhost:8080/fhir/CarePlan/careplan-maria/\$unsubscribe-due-events \
-H "Authorization: Bearer <practitioner-token>"

# Option B: Deactivate the CarePlan (stops materialization entirely)
# Update the CarePlan status to "completed" or "revoked"

In the Web UI, open the CarePlan editor and click Stop in the toolbar to disable materialization.

Configuration recommendations

SettingRecommended valueNotes
careplan-events.enabledtrueRequired for medication reminders.
scheduling.horizon-durationP14DTwo weeks ahead. Shorter than surveillance (P30D) because daily dosing creates many tasks.
scheduling.max-occurrences-per-activity3014 days of once-daily = 14 tasks per activity. Buffer to 30.
subscriptions.max-ttlP90DMatch your typical medication plan duration.
delivery.max-consecutive-failures5Disable the subscription after 5 consecutive failures.
task.auto-transition-to-readytrueRequired for webhook notifications to fire.

Further reading