Skip to main content

Message Response Tracking and Routing

When messages need responses — and those responses need to be tracked, assigned, and rerouted — use the FHIR Task resource alongside Communication. This separates message content (Communication) from the routing and assignment lifecycle (Task), allowing you to reassign work without modifying conversation data.

Assignment model

Do you need to assign work to named individuals only, to a pool by provider type, or both? That choice drives whether you use owner, performerType, or both.

Key concept

Task is the authoritative source for routing and assignment. Communication.recipient is informational — it reflects who should see the thread (for access policy scoping and display), but if there's ever a conflict between Task.owner and Communication.recipient, the Task is the source of truth. When rerouting, always update the Task first, then update Communication.recipient to match.

Communication + Task Relationship

Task Element Reference

ElementWhat It Does
focusLinks the Task to the Communication thread header it's tracking
forThe patient this Task is about (mirrors Communication.subject)
ownerWho is currently responsible — a Practitioner (individual). Cleared when rerouting to a pool.
performerTypeThe type of provider who should handle this Task (e.g. "Health coach"). Used for pool-based routing — providers whose PractitionerRole.code matches can claim the Task.
requesterWho created or triggered the Task
statusTask lifecycle: requestedacceptedcompleted (or cancelled)
businessStatusCustom status for your workflow (e.g. unassigned, claimed, escalated)
priorityUrgency level: routine, urgent, asap, stat
outputReferences the response Communication that resolved the Task

Create a Task for a Thread

When to create a Task

Does this message require a tracked response? If not, keep it as Communication only. Use Communication.category or a Bot to determine which messages need routing.

When a new message arrives that requires action, create a Task linked to the thread via focus. Use performerType to route to a provider pool, or set owner directly for individual assignment.

const task = await medplum.createResource({
resourceType: 'Task',
status: 'requested',
intent: 'order',
priority: 'routine',
focus: { reference: `Communication/${threadHeader.id}` },
for: { reference: 'Patient/homer-simpson', display: 'Homer Simpson' },
performerType: [
{
coding: [
{
system: 'http://snomed.info/sct',
code: '224535009',
display: 'Registered nurse',
},
],
},
],
requester: { reference: 'Practitioner/doctor-alice-smith' },
authoredOn: new Date().toISOString(),
});

To assign directly to a specific provider instead of a pool, set owner to the Practitioner reference and omit performerType.

tip

Only create a Task for messages that require a response. Use Communication.category or business logic in a Bot to determine which messages need routing. In production, Task creation is typically handled by a Bot triggered via a Subscription on Communication creation rather than created manually.

Claim a Task from the Pool

When a team member picks up the Task, update status to accepted and set owner to the specific Practitioner:

await medplum.patchResource('Task', task.id, [
{ op: 'replace', path: '/status', value: 'accepted' },
{
op: 'replace',
path: '/owner',
value: { reference: 'Practitioner/doctor-gregory-house', display: 'Dr. Gregory House' },
},
]);

To see unclaimed Task resources in a pool, query by performerType:

await medplum.search('Task', {
performer: 'http://snomed.info/sct|224535009',
status: 'requested',
});

Reroute to a Different Provider

Reroute target

Are you reassigning to one specific provider or back to a pool? Update owner and Communication.recipient for an individual; clear owner, set performerType, and clear recipient for a pool.

Update Task.owner and Communication.recipient to reassign the thread:

await medplum.patchResource('Task', task.id, [
{
op: 'replace',
path: '/owner',
value: { reference: 'Practitioner/dr-cardio', display: 'Dr. Cardio' },
},
{
op: 'remove',
path: '/performerType',
},
]);

await medplum.patchResource('Communication', threadHeader.id, [
{ op: 'replace', path: '/recipient', value: [{ reference: 'Practitioner/dr-cardio', display: 'Dr. Cardio' }] },
]);

Reroute to a Provider Pool

Clear Task.owner, set Task.performerType to the role type, and clear Communication.recipient. Providers whose PractitionerRole.code matches the performerType can see and claim the Task.

await medplum.patchResource('Task', task.id, [
{ op: 'remove', path: '/owner' },
{ op: 'replace', path: '/status', value: 'requested' },
{
op: 'add',
path: '/performerType',
value: [
{
coding: [
{
system: 'http://snomed.info/sct',
code: '17561000',
display: 'Cardiologist',
},
],
},
],
},
]);

await medplum.patchResource('Communication', threadHeader.id, [{ op: 'remove', path: '/recipient' }]);

To find Tasks routed to a pool:

// Task-based routing: find unclaimed Tasks in a pool by performer role
await medplum.search('Task', {
performer: 'http://snomed.info/sct|17561000',
'owner:missing': true,
_include: 'Task:focus',
});

Providers match pools via their PractitionerRole.code.

Tracking Reroute History

Audit trail

Do you need an audit trail of who owned the Task and why it was rerouted? Version history gives who/when; add Task.note or Provenance for reasons.

When Task resources are rerouted, you may need an audit trail of who owned them previously, when they were reassigned, and why. Task version history (meta.versionId) automatically captures every state change, so the basic audit trail is always available via medplum.readHistory('Task', task.id!). The question is how to capture the reason for the reroute.

Free Text Reasons

Use Task.note to append a human-readable reason on each reroute. Notes are an array, so each reroute adds an entry with the author and timestamp:

await medplum.patchResource('Task', task.id, [
{
op: 'replace',
path: '/owner',
value: { reference: 'Practitioner/dr-cardio', display: 'Dr. Cardio' },
},
{
op: 'add',
path: '/note/-',
value: {
authorReference: { reference: 'Practitioner/doctor-gregory-house' },
time: new Date().toISOString(),
text: 'Rerouting to cardiology — patient has new cardiac symptoms',
},
},
]);

Structured Reason Codes

If you need standardized, queryable reason codes, create a Provenance resource alongside the Task update. Provenance.reason accepts coded values:

await medplum.createResource({
resourceType: 'Provenance',
target: [{ reference: `Task/${task.id}` }],
recorded: new Date().toISOString(),
agent: [
{
who: { reference: 'Practitioner/doctor-gregory-house', display: 'Dr. Gregory House' },
},
],
reason: [
{
coding: [
{
system: 'https://medplum.com/CodeSystem/reroute-reason',
code: 'specialty-referral',
display: 'Specialty referral',
},
],
},
],
});

You can then query Provenance?target=Task/{id} to get the full reroute history with structured reasons.

Reroute Visibility

Visibility after reroute

After reroute, should the previous owner still see the Task (e.g. for reference), or only the new owner? That determines whether you update Task.owner in place or create a new Task for the new owner.

Task.owner is 0..1, so it can only reference a single Practitioner. When rerouting, you need to decide whether the original owner retains visibility.

Update Task in Place

If the original owner does not need to see the Task after reroute, update Task.owner directly. The original owner loses access (assuming access policies are scoped to owner):

await medplum.patchResource('Task', task.id, [
{ op: 'replace', path: '/owner', value: { reference: 'Practitioner/dr-cardio' } },
]);

Create a New Task for the New Owner

If the original owner does need to retain visibility, create a new Task for the new owner and mark the original as rerouted. Both owners can see their respective Task resources, and Task.focus links both to the same thread:

const newTask = await medplum.createResource({
resourceType: 'Task',
status: 'requested',
intent: 'order',
priority: task.priority,
focus: task.focus,
for: task.for,
owner: { reference: 'Practitioner/dr-cardio', display: 'Dr. Cardio' },
requester: { reference: 'Practitioner/doctor-gregory-house' },
authoredOn: new Date().toISOString(),
note: [
{
authorReference: { reference: 'Practitioner/doctor-gregory-house' },
time: new Date().toISOString(),
text: 'Rerouted from original Task — needs cardiology review',
},
],
});

await medplum.patchResource('Task', task.id, [
{ op: 'replace', path: '/status', value: 'cancelled' },
{
op: 'add',
path: '/note/-',
value: {
authorReference: { reference: 'Practitioner/doctor-gregory-house' },
time: new Date().toISOString(),
text: 'Rerouted to Dr. Cardio — see new Task',
},
},
]);

See Also