Message Editing and Drafts
This page covers two workflows: correcting a message that has already been sent, and saving draft messages before sending. The editing section presents both a compliance-focused retract-and-correct pattern and a simpler in-place update alternative. The drafts section covers browser-only and cross-device persistence strategies.
Message Editing and Correction
The recommended approach for healthcare messaging is the "retract and correct" pattern: mark the original as an error, then create a new Communication with the corrected content. This preserves an explicit audit trail, which is important for clinical and compliance scenarios.
If your use case does not require explicit error marking (for example, internal team chat), you can update the Communication payload in place using patchResource. FHIR version history (meta.versionId) still preserves every previous version, so the audit trail exists — it is just not surfaced via status. Choose the approach that matches your compliance requirements.
The key difference is searchability: retract-and-correct makes edits discoverable via search (Communication?status=entered-in-error finds retracted messages, Communication?category=correction finds corrections). In-place updates make edit history only accessible via _history, which is not searchable. If you need to answer "show me all edited messages" without walking version history, use retract-and-correct.
Retract the Original Message
Mark the original message as entered-in-error:
await medplum.patchResource('Communication', 'original-message-id', [
{ op: 'replace', path: '/status', value: 'entered-in-error' },
]);
Create the Corrected Message
Create a new Communication with the corrected content. Use inResponseTo to link it to the original, and tag it with a correction category. Carry forward topic, subject, and recipient from the original message so the correction is discoverable via the same searches:
const originalSender = { reference: 'Practitioner/example-sender', display: 'Example Sender' };
const correctedMessage = await medplum.createResource({
resourceType: 'Communication',
status: 'in-progress',
partOf: [{ reference: `Communication/${threadHeader.id}` }],
topic: threadHeader.topic,
subject: threadHeader.subject,
recipient: [{ reference: 'Practitioner/doctor-gregory-house', display: 'Dr. Gregory House' }],
inResponseTo: [{ reference: 'Communication/original-message-id' }],
sender: originalSender,
payload: [{ contentString: 'Updated: The appointment is at 3 PM, not 2 PM.' }],
sent: new Date().toISOString(),
category: [
{
coding: [
{
system: 'https://medplum.com/CodeSystem/communication-category',
code: 'correction',
display: 'Correction',
},
],
},
],
});
console.log(correctedMessage);
The original message content is always preserved in the resource's version history (meta.versionId), accessible via the Medplum App's History tab.
Displaying Corrections
| Approach | How it works |
|---|---|
| Hide + replace | Filter out entered-in-error messages. Show the correction in the original's position (use inResponseTo to find the link). |
| Show with indicator | Display the original with a strikethrough or "this message was edited" label. Show the correction below it. |
entered-in-error messages still appear in search results unless you explicitly filter them out with status:not=entered-in-error in your message queries.
Draft Messages
Draft messages let users compose a message and return to it later before sending.
Single-Device Drafts (Browser Storage)
For the simplest case, save draft text to browser storage keyed by thread ID:
const draftStorageKey = `draft-${threadHeader.id}`;
const composedText = 'User composed text';
localStorage.setItem(draftStorageKey, composedText);
const restoredDraft = localStorage.getItem(draftStorageKey);
if (restoredDraft) {
console.log(restoredDraft);
}
localStorage.removeItem(draftStorageKey);
In your UI, bind the restored string to your composer state (for example, a React useState setter) instead of logging it.
This is fast and requires no server round-trips, but the draft is lost if the user switches devices or clears browser storage.
If your app uses server-side rendering (for example, Next.js), guard localStorage access behind a typeof window !== 'undefined' check to avoid errors during SSR.
Cross-Device Drafts (Server-Side)
For drafts that persist across devices, use Communication.status = "preparation":
const draftBody = 'Draft body';
const serverDraft = await medplum.createResource({
resourceType: 'Communication',
status: 'preparation',
partOf: [{ reference: `Communication/${threadHeader.id}` }],
sender: { reference: `Practitioner/${currentUser.id}` },
payload: [{ contentString: draftBody }],
});
console.log(serverDraft);
Update the draft as the user types (debounce to avoid excessive requests):
const draftResourceId = 'example-draft-communication-id';
const revisedDraftText = 'Revised draft body';
await medplum.patchResource('Communication', draftResourceId, [
{ op: 'replace', path: '/payload', value: [{ contentString: revisedDraftText }] },
]);
When the user sends the message, promote the draft. At this point, also ensure topic, subject, and recipient are populated if they were not set when the draft was first created:
// Also patch in topic, subject, and recipient if they were not set when the draft was created
await medplum.patchResource('Communication', 'example-draft-communication-id', [
{ op: 'replace', path: '/status', value: 'in-progress' },
{ op: 'add', path: '/sent', value: new Date().toISOString() },
{ op: 'add', path: '/topic', value: threadHeader.topic },
{ op: 'add', path: '/subject', value: threadHeader.subject },
{ op: 'add', path: '/recipient', value: [{ reference: 'Practitioner/doctor-gregory-house' }] },
]);
Draft Communication resources support the same payload types as sent messages, including contentAttachment and contentReference. If your composer allows file uploads before send, add the attachment to the draft's payload array alongside or instead of contentString.
Loading Drafts
Query for the current user's drafts across all threads:
- TypeScript
- CLI
- cURL
// Set `sender` on draft Communications at creation time so they can be queried per user
await medplum.searchResources('Communication', {
sender: `Practitioner/${currentUser.id}`,
status: 'preparation',
});
medplum get 'Communication?sender=Practitioner/{currentUserId}&status=preparation'
curl 'https://api.medplum.com/fhir/R4/Communication?sender=Practitioner/{currentUserId}&status=preparation' \
-H 'authorization: Bearer $ACCESS_TOKEN' \
-H 'content-type: application/fhir+json'
Other users should never see your drafts. When loading messages for a thread, filter with status:not=preparation to exclude all drafts. Only load drafts via a sender-scoped query for the current user.
Server-side drafts can accumulate over time if users start composing but never send. Set up a cron-triggered Bot to purge preparation Communication resources older than a threshold (for example, 30 days). For large numbers of stale drafts, batch the deletes using medplum.executeBatch() with a Transaction Bundle for better performance.
const staleDraftCutoff = new Date();
staleDraftCutoff.setDate(staleDraftCutoff.getDate() - 30);
const staleDrafts = await medplum.searchResources('Communication', {
status: 'preparation',
_lastUpdated: `lt${staleDraftCutoff.toISOString()}`,
});
for (const stale of staleDrafts) {
await medplum.deleteResource('Communication', stale.id);
}
console.log(staleDrafts.length);
See Cron Jobs for Bots for scheduling setup.
In Your UI
For corrections, choose one of the two display approaches in the table above. If you use "hide + replace," query with status:not=entered-in-error and use the correction's inResponseTo reference to position it where the original appeared. If you use "show with indicator," render the retracted message with a visual label (for example strikethrough text or an "edited" badge) and display the correction inline below it.
For drafts, show an auto-saving indicator in the composer when using server-side drafts so users know their work is persisted. In the thread list, consider showing a "draft" badge next to threads where the current user has an unsent preparation Communication so they can resume composing.
See Also
- Searching and Querying Threads — filtering messages by
statusin thread queries - Sending Messages and Attachments
- Messaging Data Model — thread structure,
Communicationlifecycle, andtopicon child messages - Communication FHIR resource API
- Bots