A multi-state psychiatric health practice came to us running intake twice: once in Salesforce, where admissions and scheduling worked, and once in AdvancedMD, their practice-management EHR, where clinical and billing lived. Two teams keyed the same patient. Demographics drifted within weeks. Nobody could say which system was right, so staff checked both — which is how a "time-saving" CRM quietly adds headcount.
This is the architecture we shipped to fix it: a bidirectional, real-time sync built on Salesforce Platform Events, hardened across sixteen production releases. The numbers come from our own code audit of the build — 3 Platform Events, 35 Apex classes, 24 flows — and the client is anonymized. Everything else is real, including the parts that were hard.
Why EHR-CRM sync usually fails
The naive integration is easy to draw: the EHR fires a webhook, Salesforce catches it in an Apex REST endpoint, the endpoint updates the patient record. It works in the demo. Then it meets production traffic, and three things happen in sequence.
First, webhooks arrive in bursts — a morning schedule import, a batch eligibility run, a clinic opening for the day — and each synchronous handler now does DML while the platform counts every statement against governor limits. Second, those writes fire the org's existing triggers and record-triggered flows, so an innocent demographics update cascades into automation the integration author has never read, and two near-simultaneous webhooks for the same patient collide in a row lock. Third, and worst, some of those failures are silent: the EHR got its 200 response before the DML ran, so nothing retries, and the two systems drift apart one dropped webhook at a time. Drift is the failure mode that erodes trust — not the outage everyone notices, but the phone number that's right in one system and stale in the other.
The decoupling move: the handler does one job
The fix is an old integration principle applied with Salesforce-native parts: separate receiving the event from acting on it. Inbound, the REST handler does exactly one thing — validate the payload and publish a Platform Event. No DML, no queries against business objects, no trigger cascade. It returns in milliseconds, so the EHR's webhook never times out, and because publishing an event is one of the cheapest operations on the platform, the handler has almost nothing left that can fail.
Subscriber Flows do the actual writes, asynchronously, on the other side of the event bus. The bus absorbs the burst; the Flows process events at a pace the platform is happy with; and if a subscriber falls behind or errors, the event replay window means the message waits instead of vanishing. In this build the handlers are deliberately small — the demographics handler is 150 lines, the appointments handler 149, each with its own test class — because a handler that only validates and publishes has very little surface area to break.
| Layer | Component | Job |
|---|---|---|
| Inbound edge | Apex REST handlers (150 / 149 LOC) | Validate the webhook, publish the event, return fast |
| Buffer | Platform Events (3 in the build) | Absorb bursts, decouple receipt from processing, replay |
| Writes | Subscriber Flows (24 flows in the project) | Upsert Person Accounts, update Opportunities, branch on event type |
| Outbound | Account trigger + invocable Apex | Push new patients/providers to the EHR, write back the MRN |
| Config | Custom Metadata field mappings | Let admins remap EHR fields without a deployment |
| Observability | Dedicated error-log object | Make every failure visible to a human |
Inbound: a 38-field patient event
The core of the inbound design is Patient_Event__e, a Platform Event carrying 38 fields of patient demographics — names, contact details, addresses, insurance identifiers, status flags. That width is a deliberate choice. A skinny event ("patient 4127 changed, go fetch") forces every subscriber to call back into the EHR, which reintroduces callout limits, latency, and a second failure mode exactly where you removed the first one. A fat event makes each message self-contained: the subscriber Flow has everything it needs to upsert the Person Account without ever leaving Salesforce.
Person Accounts matter here. Healthcare patients are people, not companies with contacts, and modeling them as Person Accounts keeps one record per human across scheduling, billing conversations, and care history. The subscriber Flow matches on the EHR's patient identifier, updates the existing Person Account if one exists, and creates it if not — idempotently, because webhooks get redelivered and a duplicated patient is not a cosmetic bug in a psychiatric practice.
Appointments travel on their own event, Appointments_Events__e, and land on Opportunities — which sounds odd until you remember what a CRM is for. In this practice, an appointment is the revenue moment: scheduled, confirmed, completed, no-showed. Flowing appointment status onto Opportunities gave operations a real pipeline view of care delivery without anyone re-keying a schedule.
Outbound: MRN writeback closes the loop
Real-time inbound sync is only half a bidirectional integration. When the practice's own team creates a patient in Salesforce — a new intake that starts with a phone call, not an EHR record — an Account trigger hands off to invocable Apex that pushes the patient (and, through a parallel path, new providers) to AdvancedMD over its REST API.
The detail that makes this reliable is the writeback: the EHR assigns the medical record number, and the integration immediately writes that MRN onto the Salesforce record. From that moment both systems agree on identity, every future inbound event matches deterministically, and the duplicate-patient class of bugs is closed at the source. Every outbound callout failure writes to a dedicated error-log object — payload metadata, status code, timestamp, never clinical narrative — because in healthcare the second-worst outcome is failure, and the worst is silent failure.
Config over code: the Custom Metadata mapping engine
EHR schemas move. Payers demand new fields; practices customize forms; vendors rename things in API updates. If your field mappings are hardcoded in Apex, every one of those changes is a development ticket, a code review, and a deployment window.
In this build the mappings live in Custom Metadata records: EHR field on one side, Salesforce field API name on the other, with the transformation logic reading the mapping table at runtime. When AdvancedMD adds a field, an admin creates one metadata record and the sync carries it. That single decision changed the operating economics of the integration — the client's own admin handles schema drift, and we get called for architecture, not for column renames. It's the same principle we apply across integration engineering engagements: code for behavior, configuration for facts.
The PHI posture
A patient sync moves protected health information by definition, so the guardrails are part of the architecture, not an addendum. Event payloads carry the minimum necessary for the write. The error-log object stores enough to diagnose a failure and nothing a clinician typed. As a companion project, we rolled out Shield Platform Encryption over the PHI fields on Opportunities, so clinical context is encrypted at rest without breaking the automation that reads it.
And we say "HIPAA-conscious," not "HIPAA compliant," deliberately: compliance is a property of the practice's program — their BAA, their policies, their training — not a claim a consultant can make on their behalf. What we control is whether the integration is ever the weak link.
Hardened, not launched: 16 production releases
The repository for this build contains 16 dated pre-deployment snapshots. That's not an ornament; it's the honest fingerprint of what real-time healthcare integration takes. Webhook payloads showed up with fields the documentation didn't mention. Event subscribers needed reordering once real volume exposed a race. The mapping engine grew branches for edge cases no one listed in discovery. Each snapshot marks a production release where the system got more boring — which is the goal. Anyone selling you a one-shot EHR integration is describing the demo, not the system. This build is one of four behavioral-health platforms we've written up in Salesforce for behavioral health, and the pattern — events as shock absorbers, config over code, error logs as first-class objects — repeats across all of them.
Scoping your own EHR integration: the checklist
- Inventory the events, not the fields. What does the EHR emit — patient created, demographics changed, appointment status? The event list defines the integration; fields are just payload.
- Decide the system of record per attribute. Demographics from the EHR, engagement from the CRM, identity via MRN writeback. Undecided ownership is how drift starts.
- Demand idempotency everywhere. Webhooks redeliver and callouts retry. Every write must be safe to run twice.
- Buffer inbound traffic through Platform Events. Keep DML, triggers, and governor limits out of the webhook handler's execution context entirely.
- Put mappings in Custom Metadata. Schema drift is certain; deployments for column renames are optional.
- Fund the error path. A dedicated log object, an alert rule, and a human who owns the queue. In healthcare, silent failure is the expensive kind.
If the integration also needs to trigger downstream work — census recalculation, document generation, follow-up cadences — that's the adjacent discipline of Salesforce automation, and the event-driven spine above is exactly what makes it safe to build on. The broader context for healthcare operators is on our Salesforce for healthcare page.
Frequently asked questions
Can Salesforce integrate with AdvancedMD?
Yes. AdvancedMD exposes webhooks for patient and appointment events and a REST API for writes. We run a bidirectional sync in production: webhooks land on an Apex REST endpoint that publishes Platform Events inbound, and invocable Apex pushes new patients and providers to AdvancedMD outbound, with the EHR-assigned MRN written back to Salesforce. The same shape works for most EHRs with an API.
Do we need middleware like MuleSoft or Boomi for an EHR-Salesforce integration?
Not for a point-to-point sync like this one. Platform Events give you the buffering, async processing, and replay that middleware is usually bought for — natively, with no additional license or vendor in the PHI path. Middleware starts earning its cost when many systems need the same events or when heavy transformation has to happen outside Salesforce.
Why publish a Platform Event instead of writing records directly from the webhook handler?
Because webhooks arrive in bursts and synchronous handlers doing DML inherit every trigger, flow, and governor limit on the objects they touch. A handler that only validates and publishes returns in milliseconds and almost never fails. Subscriber Flows then do the writes asynchronously, at a pace the platform manages, with replay if a subscriber falls behind.
Is this kind of EHR sync HIPAA compliant?
Compliance is a property of your program — your BAA with Salesforce, your policies, your training — not something an integration architecture can claim on its own. What we build is HIPAA-conscious: minimum-necessary payloads, Shield Platform Encryption on PHI fields, error logs that never store clinical narrative, and test classes on every code path that touches patient data.
What happens when the EHR adds or renames a field?
In this build, nothing that requires a developer. Field mappings live in Custom Metadata records — EHR field name on one side, Salesforce field API name on the other. An admin adds or edits a mapping record and the sync picks it up. No deployment, no code review, no release window.
Bring us the two systems that don't agree
GAT Solutions builds revenue-generating systems for healthcare, accounting, and SaaS companies — fast, flawlessly, and powered by AI.
If your staff keys patients into two systems, or your CRM and EHR each hold a different version of the truth, bring both to a working session. We'll map the event flows and show you the architecture before you spend a dollar.