Skip to main content

Messaging Automations

Prerequisites

The patterns on this page use Medplum Bots — an advanced feature that is disabled by default on many projects — and FHIR Subscriptions. Recurring automations also use cron-scheduled Bots (Cron Jobs for Bots). Confirm Bots are enabled for your project and that you can create Bots and Subscriptions (typically as a Project administrator).

Medplum Bots and FHIR Subscriptions let you automate messaging workflows without manual intervention, while still giving you full control over your business logic. There are two main design patterns:

  • Real-time automations — A Bot triggered by a Subscription on Communication when a message is sent. This page walks through out-of-office rerouting: checking provider availability at message time and returning work to a pool when the assignee is unavailable. Other common real-time patterns include creating or completing Tasks when messages arrive; you can apply the same Subscription shape and tailor the Bot logic.
  • Recurring automations — A Bot running on a cron schedule. The Bot periodically scans existing resources to find conditions that need action (e.g., threads with no response for N days).

Both patterns let you encode your own rules — who gets a Task, what SLA thresholds to enforce, when to escalate — while keeping the automation infrastructure out of your application code.

Real-Time Automations

A real-time automation uses a Subscription on Communication to trigger a Bot whenever a message is sent in a thread. Because the Bot runs at message creation time, it can inspect the current state of the system: who sent the message, which Task (if any) is focused on the thread, and whether the assigned provider is available.

Example: OOO rerouting

When a message arrives for a provider who is currently out of office, a Bot can automatically reroute the Task back to the provider pool so another team member can pick it up.

The Bot uses the Medplum scheduling model to determine availability. It looks up the assigned provider's Schedule and calls $find for the current time window. If no free slots are returned — whether because of explicit time blocks (vacation, holidays) or because the provider has no availability defined for the current day — the Task is rerouted to the pool.

Bot Code

// Bot: reroute Tasks to the pool when the assigned provider is out of office.
// Uses Schedule $find to check availability at message receive time.
export async function oooRerouteHandler(medplum: MedplumClient, event: BotEvent<Communication>): Promise<void> {
const message = event.input;
const threadRef = message.partOf?.[0]?.reference;
if (!threadRef) {
return;
}

const openTasks = await medplum.searchResources('Task', {
focus: threadRef,
status: 'requested,accepted',
});

if (openTasks.length === 0) {
return;
}

const rerouteTask = openTasks[0];
if (!rerouteTask.id) {
return;
}
const ownerRef = rerouteTask.owner?.reference;
if (!ownerRef) {
return;
}

const schedules = await medplum.searchResources('Schedule', {
actor: ownerRef,
});

if (schedules.length === 0) {
return;
}

const schedule = schedules[0];
if (!schedule.id) {
return;
}

const now = new Date();
const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
const result: Parameters = await medplum.post(medplum.fhirUrl('Schedule', schedule.id, '$find'), {
resourceType: 'Parameters',
parameter: [
{ name: 'start', valueDateTime: now.toISOString() },
{ name: 'end', valueDateTime: oneHourLater.toISOString() },
],
});

const bundle = result.parameter?.[0]?.resource as Bundle<Slot>;
const freeSlots = bundle?.entry?.filter((e) => e.resource?.status === 'free') ?? [];

if (freeSlots.length > 0) {
return;
}

// Provider is unavailable — reroute Task back to the pool
const threadHeaderId = threadRef.split('/')[1];
await medplum.patchResource('Task', rerouteTask.id, [
{ op: 'remove', path: '/owner' },
{ op: 'replace', path: '/status', value: 'requested' },
{
op: 'add',
path: '/note/-',
value: {
text: `Auto-rerouted: ${rerouteTask.owner?.display ?? ownerRef} is currently unavailable`,
time: now.toISOString(),
},
},
]);
await medplum.patchResource('Communication', threadHeaderId, [{ op: 'remove', path: '/recipient' }]);
}
tip

This example reroutes to the performerType pool. You could instead reroute to a specific coverage provider by setting Task.owner to that provider's reference. You can also customize the availability check — for instance, querying a different serviceType or adjusting the time window.

Deploy and Subscribe

  1. Create a Bot resource in the Medplum App (Project Admin → Bots → New). See Deploying Bots for full steps.
  2. Create a Subscription that triggers the Bot for new child messages:
await medplum.createResource({
resourceType: 'Subscription',
status: 'active',
reason: 'Reroute Tasks when assigned provider is out of office',
criteria: 'Communication?part-of:missing=false&status=in-progress',
channel: {
type: 'rest-hook',
endpoint: 'Bot/{your-ooo-reroute-bot-id}',
},
});

The criteria part-of:missing=false limits the Subscription to child messages (which have partOf), not thread headers. status=in-progress avoids firing on retracted messages (entered-in-error) or drafts (preparation) — it targets newly sent messages.

Verify

  1. Configure a provider Schedule with no free slots for the current window (or an explicit OOO block).
  2. Ensure an open Task for the thread is owned by that provider, then send a new child message in the thread so the Subscription runs.
  3. Confirm the Task has owner removed, status back to requested, and an auto-reroute note; optionally confirm thread recipient was cleared as in the example.

Recurring Automations

A recurring automation uses a cron-triggered Bot to periodically scan existing resources and take action on conditions that have developed over time. This pattern is well suited for SLA enforcement, reminders, and cleanup tasks where the trigger is the passage of time rather than a specific event.

Example: Stale Thread Reminders

A cron-triggered Bot can periodically scan for threads without timely responses and create reminder Tasks for your queue or follow-up workflows.

Bot Code

// Bot (cron-triggered): find threads with no activity for N days, create a reminder Task per thread if none exists. Export as 'handler' when deploying.
export async function staleThreadRemindersHandler(medplum: MedplumClient, _event: BotEvent): Promise<void> {
const staleDays = 3;
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - staleDays);

const staleThreads = await medplum.searchResources('Communication', {
'part-of:missing': true,
status: 'in-progress',
_lastUpdated: `lt${cutoff.toISOString()}`,
});

for (const thread of staleThreads) {
if (!thread.subject) {
continue;
}
const existingTasks = await medplum.searchResources('Task', {
focus: `Communication/${thread.id}`,
code: 'https://medplum.com/task-codes|respond-to-old-message',
status: 'requested,accepted',
});

if (existingTasks.length > 0) {
continue;
}

await medplum.createResource({
resourceType: 'Task',
status: 'requested',
intent: 'order',
priority: 'urgent',
code: {
coding: [
{
system: 'https://medplum.com/task-codes',
code: 'respond-to-old-message',
display: 'Respond to old message',
},
],
},
focus: { reference: `Communication/${thread.id}` },
for: thread.subject,
description: `Thread "${thread.topic?.text ?? 'Untitled'}" has been open for ${staleDays}+ days without a response`,
authoredOn: new Date().toISOString(),
});
}
}

The 3-day threshold is configurable — adjust based on your response time expectations per thread category and urgency.

Deploy

Deploy the Bot and configure it to run on a cron schedule (e.g., once per hour, once per day). See Cron Jobs for Bots.

Analytics and SLA Tracking

FHIR search is designed for clinical data lookups, not aggregate analytics. Metrics like response time, SLA compliance, and escalation rates are best calculated by exporting Task data to your analytics platform (e.g. BigQuery, Snowflake, Redshift).

Key data points available on each Task:

  • Task.authoredOn — when the Task was created
  • Task.status and status change timestamps — track time-to-claim and time-to-complete
  • Task.owner — who handled it
  • Task.priority — urgency level at creation and any subsequent changes
  • Task.focus — which thread the Task is about

To surface urgency in your UI without building SLA infrastructure, use Task.priority (routine, urgent, asap, stat) to drive visual indicators like color coding and sort order in your task queue.

See Also