Skip to main content

Beta Changelog

Beta

Medplum Scheduling APIs are currently in beta. While in beta, we may make backwards-incompatible changes to fix bugs or align behavior with the FHIR spec. This page is a running log of those changes, newest first, along with the migration steps (if any) needed to adopt them.

Slot.serviceType is now respected on busy Slots

Target release: September 1, 2026  ·  Issues: #9995, #9998

What changed

Previously, $find and $book only applied the serviceType filter to free Slots. Every busy / busy-unavailable / busy-tentative Slot was subtracted from availability regardless of its serviceType, so a busy Slot scoped to one service actually blocked the entire Schedule.

This contradicted the documented behavior:

  • With serviceType: Blocks only that specific service
  • Without serviceType: Blocks all services

Starting after September 1, the serviceType filter is applied to busy Slots as well:

  • A busy Slot without a serviceType still blocks all services (unchanged).
  • A busy Slot with a serviceType now blocks only the matching service(s).

Thank you to Nick Catalano for reporting the issue.

Who is affected

Users of $find / $book / $hold before Medplum Server v5.1.28 will have had Slot resources created with a serviceType attribute.

These slots were intended to block their related Schedule availability for all services ("wildcard" service type matching), but before #9998 the slots were being created with serviceType set to the type of the linked Appointment resource.

While the bug above existed, that stray serviceType was ignored, so those Slots still blocked everything and no harm was done.

Once this fix lands, those Slots will block only their single service — leaving the provider bookable for other services at the same time and allowing double-booking. If you used $find / $book before #9998 shipped, you likely have such Slots and should run the migration below.

You are also affected if you authored busy / busy-unavailable / busy-tentative Slots with a serviceType and relied on them blocking every service. If you never set serviceType on busy Slots, or you always intended a busy Slot to block only its referenced service, no action is needed.

Migration

To preserve the old "block all services" behavior, clear serviceType on the affected busy Slots before upgrading. Since blocked time in the past no longer matters, you only need to migrate Slots ending after your upgrade date.

The script below finds every busy / busy-unavailable / busy-tentative Slot with a serviceType ending after TARGET_DATE and removes the serviceType field, so those Slots continue to block all services.

// Migration for medplum/medplum#9995 (and #9998): clear `serviceType` on busy /
// busy-unavailable Slots ending after a target date, so they keep blocking
// ALL services (the pre-fix behavior) instead of only the matching service.
// Chiefly cleans up Slots that $find/$book wrote a serviceType onto before
// #9998, which would otherwise allow double-booking once #9995's fix lands.
//
// export MEDPLUM_BASE_URL='https://api.medplum.com/'
// export MEDPLUM_CLIENT_ID='...'
// export MEDPLUM_CLIENT_SECRET='...'
// export TARGET_DATE='2026-09-01T00:00:00Z' # your upgrade date
//
// npx tsx clear-busy-slot-service-type.ts
//
// Dependencies: npm i @medplum/core tsx
import { MedplumClient } from '@medplum/core';

async function main(): Promise<void> {
const targetDate = new Date(process.env.TARGET_DATE as string);
const medplum = new MedplumClient({ baseUrl: process.env.MEDPLUM_BASE_URL });
await medplum.startClientLogin(process.env.MEDPLUM_CLIENT_ID as string, process.env.MEDPLUM_CLIENT_SECRET as string);

// Phase 1: gather affected slot IDs (fully drained before we mutate anything).
// `service-type:missing=false` limits the scan to slots that carry a serviceType;
// `end=gt...` uses Medplum's custom Slot end search parameter.
const ids: string[] = [];
for await (const page of medplum.searchResourcePages('Slot', {
status: 'busy,busy-unavailable,busy-tentative',
'service-type:missing': 'false',
end: `gt${targetDate.toISOString()}`,
_count: 1000,
})) {
for (const slot of page) {
if (slot.id) {
ids.push(slot.id);
}
}
}
console.log(`Found ${ids.length} slot(s) to update.`);

// Phase 2: clear serviceType on each with a targeted JSON Patch (no re-read).
for (const id of ids) {
await medplum.patchResource('Slot', id, [{ op: 'remove', path: '/serviceType' }]);
console.log(`Updated Slot/${id}`);
}
}

main().catch((err) => {
console.error(err);
process.exit(1);
});