Legacy product · Patient letters
How auto-send letters work
The legacy product automatically emails or texts patients a link to a cost-estimate
(or Good Faith Estimate) letter for their upcoming visit. Everything about who gets one,
when, and what it says is driven by a list of AutoSendLetterConfig rows.
This is a tour of that behavior and every knob on the config page.
The one thing to understand first: an email or SMS never carries the letter. It carries a login link. The patient portal is where the letter actually lives and is read — the notification just says “you have a letter, tap here.”
And a letter only exists once eligibility and pricing are done: it needs the visit to be calculated, have benefits applied, and a scheduled time. Letters sit at the very end of the pipeline — most “why didn’t it send” answers are really “the visit wasn’t ready yet.”
Mental model · 01
There is no global “auto-send” switch — only configs
The task engine is deliberately dumb. It loops over visits and asks each config
“is this one yours?” Everything anyone can tune lives on an AutoSendLetterConfig,
and each config answers two independent questions: which visits are mine? (the Filters panel) and
what do I do with them? (the Letter Properties, Auto Resend, and Priority panels).
The second clarifier resolves most of the team’s confusion: a letter can go out on three different time-axes, and they are constantly conflated.
1 · First send
automatic — no field
A qualifying, ready visit with no letter yet gets one generated and sent, once. Nothing configures this beyond qualification itself.
2 · Regenerate & resend
field: New Letter Conditions
The visit changed in a way you care about (charges, insurance, re-run benefits…) → tear up the old letter, build a fresh one with new numbers, send again.
3 · Remind
field: Auto Resend Options
Same letter, but the patient never opened it → nudge them again. Stops the moment the portal reports the letter was viewed.
The naming trap
“New Letter Conditions” and “Auto Resend Options” both sound like “resend,” but they are completely separate mechanisms — one makes a new letter because facts changed, the other re-notifies about the existing letter. This mislabeling is the single biggest source of confusion. See Resend vs remind.
Runtime · 02
The qualify → generate → send pipeline
Three scheduled flows share one engine (AutoSendLetters.SendLetters()). They differ
only in which visits they feed it: the batch flow processes visits staged during the daily
processing run; the future-visits flow scans live upcoming appointments (self-gated to ~once/day);
the reminder flow re-notifies unviewed letters and never generates. Every candidate visit walks
these steps.
-
Match a config first match wins
Walk the priority-sorted configs; the first one the visit and patient qualify for is used, and no others are considered. No match → logged as
VisitNotQualified. -
Check the visit is ready
Must be
Calculated, haveBenefitsApplied, and aScheduledTime. Not ready → no letter (recorded asNotCalculated/BenefitsNotApplied). This is why ~99% of send-events are gating reasons, not failures. -
Decide: generate?
Generate if there’s no letter yet, or the patient can receive it and this isn’t a generate-only config, or a tracked change means it needs regeneration. Renders the template with live patient / charges / benefits / payment data and stores the HTML.
-
Decide: send?
Send only if there are zero errors and the config isn’t Auto Generate Only. Also requires consent + a valid email or SMS phone.
-
Send & record 3-channel fan-out
Mark the letter sent, push it to the portal, fire the email/SMS notification, flag
Visit.LetterSent, and enqueue an HL7 outbound row. Details below.
Dedup is state-based, not a key
There is no idempotency key anywhere. The only guard against re-sending is the
Visit.LetterSent flag. In the batch flow that flag is written to a staging table, so if
a run dies after the notification but before the batch is committed, the flag is lost and the letter can
re-send next run. The manual “send” button in the UI has no already-sent guard at all.
Delivery · 03
What a “send” actually does
One send fans out to up to three channels. Only one of them transmits the letter’s content.
The full letter (HTML, invoices, patient, facility) is pushed to the customer’s patient-portal API. This is the channel the patient actually reads. If it fails, the whole send aborts.
A link-only notification — email via SES/SMTP, or SMS via Telesign/Twilio with a shortened link. It carries no letter content, just the portal login link.
Every send also enqueues an HL7OutboundLetter. A separate nightly task renders a
PDF + ORU^R01 to a file drop — the only channel that transmits the actual content to an
external system.
Channel choice is automatic
You don’t pick email vs SMS per config. The sender factory chooses based on the patient’s
communication preference and which contact info is actually valid, falling back silently. There is
no print/mail vendor — Mail is only a manual “staff mailed paper themselves” stamp.
Configuration · 04
The complete configuration surface
Every field on the AutoSendLetterConfig page, grouped by its four panels. This is
the part people most want enumerated. Property names (in mono) are the underlying fields.
Multi-select dimension lists. Within one list it’s an OR (any match).
An empty list means “all match,” not “none.” This is the #1 gotcha.
Insurance, facility, visit-status and visit-type filters are all used in the wild;
Providers is supported but used by no customer (0 of 52 configs).
How far ahead the appointment is scheduled.
Bounds on the requested deposit.
Bounds on the patient’s estimated responsibility.
Age bounds — with a quirk: a patient with no birthdate on file auto-qualifies.
Only shown if the customer has GFE enabled. Filters to All / GFE-only / non-GFE visits.
Customer-defined filters injected here — not built-in fields, so two customers’ Filters panels can look completely different. In the wild these gate on things like coverage status, “estimate created,” or active Medicaid/Medicare flags. Six customers use them.
required The letter template. Only active visit templates are selectable; if the chosen template is later archived, the whole config is silently skipped at runtime (a red banner warns you on the page).
The set of visit changes that trigger a regenerate + resend of an already-sent letter (charges, insurance, benefits re-run, reschedule, etc.). See Resend vs remind.
Rendering toggle for the letter content.
Whether generating the letter also creates an invoice.
Generate & store the letter but never notify the patient — a staff-review / send-later workflow. Not niche: ~65% of all configs fleet-wide (34 of 52) run in this mode, and some customers use the feature exclusively this way.
Master switch for reminders.
0 to 5. The house default in the wild is 2; the most aggressive is a statement-dunning config at 5.
Days after the initial send, and between each nudge (commonly 3). Reminders stop when the patient views the letter or the max is reached.
Lower numbers are checked first, and the first qualifying config wins — no others run. Blank sorts last, and among blanks the order is not guaranteed. In practice no customer sets Priority — it’s blank on all 52 configs — so everyone relies on their filters being non-overlapping instead.
In the wild · 05
Three ways customers actually use it
Adoption is about 11 of 29 ClarityFlow customers (~38%) — auto-send letters is a ClarityFlow feature, so RevFind-only customers are excluded from the count. Among those adopters, the feature isn’t used the same way at all: the same panels compose into three recognizable shapes — the clearest way to understand what the knobs are for.
Send now, gate lightly
e.g. a high-volume radiology group
Match on insurance + facility and little else; generate and send immediately.
Simple and high-throughput.
filters: insurance · facility → auto-send
Generate for staff, don’t send
e.g. a GI practice — its entire config set
Auto-Generate-Only. Tightly gated: a 14–60 day window, by visit type, and by
custom coverage flags (Medicaid/Medicare). Letters are staged for staff to review, never
auto-delivered.
generate-only · days + visit-type + custom-props
GFE + reminders + deposit
e.g. a multi-facility ortho group
Excludes GFE visits, requires a deposit > $0, sends 2–12 days out, and
nudges twice (3 days apart) if unviewed.
auto-send · MinDeposit>0 · reminders 2×/3d
Two patterns worth carrying away
~65% of all configs (34 of 52) are generate-only — for many customers this is a staff tool that stages estimates for review, not an automatic patient message. And two capabilities are supported but used by nobody in the fleet: Priority ordering and the Providers filter (0 of 52 configs). Good to know they exist, and that reaching for them is uncharted territory.
Try it · 06
See it work
Three small sandboxes for the two questions. They run on the three real archetypes above. Nothing here talks to a server — it’s a model of the matching and lifecycle logic to build intuition.
Archetype explorer — what do I do with them?
Pick an archetype to see its full config — the filters that decide which visits qualify and the result options that decide what happens — plus a plain-English reading.
Filters — which visits qualify
Result — what happens
In plain English: send a commercial estimate to any scheduled commercial-insurance visit at Main Imaging, right away.
Qualification playground — which visits are mine?
Adjust the sample visit and watch which config catches it. Configs are checked top-to-bottom and the first match wins — so a lower one can “would-match” but be shadowed. Green criteria pass, red fail; an empty filter matches everything.
Sample visit
Configs · checked in order
Disambiguation · 07
“New Letter Conditions” vs “Auto Resend Options”
The two controls people most often mix up. One reacts to facts changing; the other reacts to the patient not looking.
Regenerate & resend
New Letter Conditions · VisitResendCriteria
- Answers: “the situation changed — is the old estimate now wrong?”
- Builds a brand-new letter with fresh numbers and sends it.
- Fires from the batch / future-visit runs when a tracked
VisitChangeEventreason intersects your selection. - Triggers include: Insurance, Charges, Deposit, Provider, Facility, Estimate, Patient Amount, New Quote, Benefits status, Scheduled Time, Visit Type, Status, Custom Properties.
Remind
Auto Resend Options · AttemptResend
- Answers: “nothing changed, but they never opened it — poke them again.”
- Re-sends the notification for the same letter, same numbers.
- Fires from the reminder task on your day-interval, up to Max Reminders.
- Stops the moment
ViewedDateis set — the portal reports back when the patient opens the letter.
So one patient can receive: a first letter, a replacement letter if their charges change, and reminders about whichever letter is current — three distinct triggers, three distinct controls.
They’re genuinely independent knobs
A real config in the fleet sets New Letter Conditions (regenerate on Insurance/Facility change) while leaving reminders off — so those changes drive a fresh letter, but no timed nudges ever go out. You can use either, both, or neither. Fleet-wide the most common regenerate triggers are Charges, Insurance, and Estimate (9 configs each), then Patient Amount (7), Deposit (6), and Facility (5).
Send-axis simulator — watch one letter’s lifecycle
Drive one visit through time. This config regenerates on Charges or Estimate changes and sends 2 reminders, 3 days apart. Notice a provider change does nothing (not in its New Letter Conditions), a charges change makes a new letter, and reminders only fire while the letter is unviewed.
config · resend on: Charges, Estimate · reminders: max 2, every 3 days
- day 0First send — estimate pushed to portal, email/SMS link sent
Gotchas · 08
Where the mental model breaks
The behaviors that surprise people, and the hard preconditions no config can override.
Empty filter = everything
A blank Insurance / Facility / etc. list means all match, not none. People leave it blank thinking it excludes.
First match wins
Overlapping configs don’t stack — exactly one wins per visit, decided by Priority (blank = last, unordered).
No birthdate → passes age
A patient with no birthdate on file auto-qualifies for any age filter, rather than being excluded.
Letters live downstream
No config toggles it off: a visit must be calculated + benefits-applied + scheduled before any letter generates.
Archived template = silent skip
Point a config at an archived template and it’s skipped entirely at runtime (with an on-page warning banner).
Consent + contact required
Needs electronic-communication consent and a valid email or SMS phone — enforced in code, not a config field.
Manual send has no guard
The UI “send” button re-pushes, re-notifies, resets viewed/void, and enqueues another HL7 row on every click.
Dev sends are live
On this branch there’s no environment whitelist — auto-send against a dev DB with real contact info hits real phones/emails.
The send-event ledger reads scarier than it is
PatientLetterSendEvent logs one row per visit, per run, per evaluation, so at a busy customer it
grows into the millions of rows. The vast majority carry an “error,” but these are overwhelmingly
gating reasons (BenefitsNotApplied, VisitNotQualified,
NotCalculated, LetterAlreadyAutoSent), not delivery failures. Genuine failures — no
consent, bad contact, portal error — are a tiny tail.
Reference · 09