MDClarity feature explainers  / explainers / auto-send letters

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.

Source: MDClarityCore on cary/eligibility-and-letters · usage surveyed across all customer production mirrors · 2026-07-09 · structure & behavior only, no patient data

Customers using it
11 of 29
~38% of ClarityFlow customers (RevFind-only excluded)
Configs fleet-wide
52
3 customers hold 79% of them
Generate-only
65%
34 of 52 configs generate but never notify
Reminders enabled
11
configs; house default 2 nudges, 3 days apart
Built but unused
2
Priority & Providers — 0 of 52 configs use them

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, have BenefitsApplied, and a ScheduledTime. Not ready → no letter (recorded as NotCalculated / 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.

1Portal push

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.

2Email or SMS

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.

3HL7 queue

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 vendorMail 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.

① Filters“which visits qualify” — all AND-ed together
Insurances / Facilities / Providers / Visit Statuses / Visit Types List<string>

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).

Min / Max Days OutMinDaysOut · MaxDaysOut

How far ahead the appointment is scheduled.

Min / Max Deposit AmountMinDepositAmount · MaxDepositAmount

Bounds on the requested deposit.

Min / Max Total Patient AmountMinTotalPatientAmount · MaxTotalPatientAmount

Bounds on the patient’s estimated responsibility.

Min / Max Patient AgeMinPatientAge · MaxPatientAge

Age bounds — with a quirk: a patient with no birthdate on file auto-qualifies.

GFE Required VisitsApplyToGoodFaithEstimateVisits

Only shown if the customer has GFE enabled. Filters to All / GFE-only / non-GFE visits.

Custom propertiesCustomProperties

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.

② Letter Properties“what letter, and when to remake it”
Template to UseLetterTemplate

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).

New Letter ConditionsVisitResendCriteria

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.

Show Bill Type?ShowBillType

Rendering toggle for the letter content.

Make Invoice?MakeInvoice

Whether generating the letter also creates an invoice.

Auto Generate Only?AutoGenerateOnly

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.

③ Auto Resend Optionsactually reminders — nudges to view the same letter
Send Automatic Reminders?AttemptResend

Master switch for reminders.

Max RemindersMaxResendAttempts

0 to 5. The house default in the wild is 2; the most aggressive is a statement-dunning config at 5.

Days Between RemindersResendInterval

Days after the initial send, and between each nudge (commonly 3). Reminders stop when the patient views the letter or the max is reached.

④ Prioritytie-break when a visit could match more than one config
PriorityPriority (int, nullable)

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

InsuranceCommercial
FacilityMain Imaging
Everything elseany (empty = all)

Result — what happens

DeliveryAuto-send
Remindersoff
TemplateCommercial Estimate

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

1Send now, gate lightlyauto-send
insurance = Commercialfacility = Main Imaging
2Generate for staffgenerate-only
insurance = Self Paytype ∈ CT, MRIdays 14–60age ≥ 18
3GFE + reminders + depositauto-send + remind
non-GFE onlydeposit > $0days 2–12
Adjust the visit to see which config catches it.

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 VisitChangeEvent reason 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 ViewedDate is 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
Day0
Letterv1 · sent
Viewedno
Reminders0 / 2

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

Glossary of the moving parts

AutoSendLetterConfig
The rule row. Holds every filter, the template, the resend/reminder settings, and priority. The whole feature is a list of these.
PatientLetter
The generated artifact. Rendered HTML is stored once and served as-is — never re-rendered on view. Tracks SentDate / ViewedDate / DeliveryMethod.
PatientLetterSendEvent
Append-only audit ledger, one row per evaluation attempt. Empty error list = success; failure reasons live in its XML body.
VisitChangeEvent
Records what changed on a visit (charges, insurance, benefits…). Its reasons are matched against a config’s New Letter Conditions to trigger regeneration.
Visit.LetterSent
Promoted flag on the visit — the only dedup guard for auto-send. Also LetterRequired and AutoSendLetterConfigKey.
PatientUser
The portal account for a patient. Carries the communication preference the sender factory reads to pick email vs SMS.
HL7OutboundLetter
Queue row enqueued on every send; a nightly task turns it into a PDF + ORU^R01 file — the only channel that transmits letter content externally.
ViewedDate
Pulled back from the portal when the patient opens the letter. Setting it stops reminders.
Auto Generate Only
Config mode that produces & stores a letter but never notifies — for staff review or a manual send later.