2026-05-15 — OdontoX onboarding email campaign
Status: Design approved, ready for implementation plan Owner: Sarmad Brand: “Letters from Sarmad” — founder-personal voice from [email protected] A behavioral onboarding email system for trialing and newly-paid OdontoX clinics. Sends one short, professionally-written email per day per clinic, mapped to activation signals already in the database. Every primary CTA links to aq.odontox.io help article.
1. Goal
Convert more trials into paid clinics, and get new paid clinics using 80% of the relevant product surface within 30 days — without ever feeling “marketing-y.” Success is measured by:- Trial-to-paid conversion lift (campaign on vs. off, via the per-clinic superadmin toggle).
- Activation depth: % of clinics that have used patients + appointments + charting + WhatsApp within the trial window.
- Replies to campaign emails (these emails reply directly to Sarmad’s inbox and are themselves the most valuable signal).
2. Audience & gating
A clinic is in the campaign when all of the following are true:clinics.is_test_account = falseclinics.marketing_campaign_enabled = true(new column — superadmin per-clinic toggle, defaultstrue)clinics.marketing_unsubscribed = false(new column — clinic-side opt-out, defaultsfalse)- One of:
subscription_status = 'trial'ANDtrial_end_date > NOW(), orsubscription_status = 'active'ANDactivated_at > NOW() - INTERVAL '30 days'
role = 'admin'. Receptionists, doctors, accountants are excluded. Multiple admins in one clinic each receive the email.
Out-of-scope (handled by existing transactional system): trial-started, trial-expiring, trial-extended, subscription-ended, payment-failed, password reset, OTP, staff invitation, patient invitation, invoice/quotation/appointment notifications.
3. Voice & design rules
The single instruction: letters, not emails. Every rule below exists to defend that.Voice
- First-person from Sarmad (“Sarmad here,” and “we” for the team, never “I” for clinic-wide statements).
- Address recipients by first name only. Never “Dear Dr. Khan.”
- Professional sentence-case prose. Some warmth, the occasional dry remark, never forced casualness.
- Short paragraphs (1–3 lines). One idea per paragraph. White space carries meaning.
- Never apologize for “interrupting.” Never say “we hope this finds you well.”
- Reply-to is
[email protected]. Replies are the feature. - No urgency manipulation. No “ONLY 3 DAYS LEFT!!!” language.
- Always sign ”— Sarmad, OdontoX” (text, not an image; not “Sarmad Hussain”).
- Always one P.S. line — italic, muted color. Personal, small, occasionally a delight.
Subject lines
- Sentence case. Properly written. Never all-lowercase, never all-caps.
- ≤ 8 words.
- No emoji.
- Specific, not generic. Lead with the value or the action, not “OdontoX update.”
Visual rules
- Logo at the top of every email body:
ui/public/email.png(stacked badge + “odontoX” wordmark, served at 36px height). - The Ruby email (
ai_insights) usesui/public/ruby.pngat the top instead, giving Ruby her own visual identity within the campaign. - Footer logo on every email:
ui/public/logo.png(standard app wordmark), centered, 20px tall, 65% opacity. - System font stack only:
-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif. No web fonts loaded. - Body
#111on#fff. Asides and P.S.#9a9a9a. CTA is the only use of brand green#0C8C5E. - Line height 1.6. 560px max content width. Generous padding.
- One CTA per email. Plain underlined link, not a button. Links signal “person wrote to me”; buttons signal “marketing email.”
- Footer is two muted lines: “OdontoX · Karachi · Unsubscribe · Email preferences” beneath the small footer logo.
- Plain-text fallback is generated from the HTML and is ~95% identical (because the HTML is already mostly prose).
- All variables (
{firstName},{patientName},{patientCount}) resolved server-side before send. Never tokens-in-production.
Personalization variables
| Variable | Source |
|---|---|
{firstName} | users.first_name of the admin recipient |
{patientCount} | SELECT count(*) FROM patients WHERE clinic_id = $1 AND deleted_at IS NULL |
{patientName} | Most recently-added patient’s first name (used in first_patient_added) |
{appointmentCount} | live count |
{clinicName} | clinics.name |
4. Lesson library — 17 emails
Each lesson is a self-contained module exporting{ key, priority, phase, trigger, articleUrl, template }. Adding lesson #18 is one new file plus one new article on q.odontox.io.
Phase 1 — Trial (Day 0–14)
| # | Key | Trigger | Priority | Subject | Article |
|---|---|---|---|---|---|
| 1 | welcome | Signup confirmed AND patients = 0 | 100 | Welcome to OdontoX | q.odontox.io/guides/welcome |
| 2 | first_patient_added | patients ≥ 1, sent next morning | 95 | Your first patient is in | q.odontox.io/guides/appointments |
| 3 | first_appointment | patients ≥ 3 AND appointments = 0 | 90 | Booking your first appointment | q.odontox.io/guides/appointments |
| 4 | whatsapp_off | Day ≥ 3 AND WhatsApp module off AND patients ≥ 5 | 85 | Connect WhatsApp to cut no-shows by 40% | q.odontox.io/guides/whatsapp-setup |
| 5 | dental_charting | appointments ≥ 1 AND chart_entries = 0 | 80 | Using the dental chart | q.odontox.io/guides/dental-chart |
| 6 | clinical_notes | appointments ≥ 1 AND clinical_notes = 0 AND day ≥ 4 | 75 | Clinical notes, the fast way | q.odontox.io/guides/clinical-notes |
| 7 | invite_staff | user_count = 1 AND day ≥ 5 | 70 | Add your team to OdontoX | q.odontox.io/guides/invite-staff |
| 8 | mobile_app | No mobile session ever AND day ≥ 6 | 65 | OdontoX on iOS and Android | q.odontox.io/guides/mobile-app |
| 9 | first_invoice | Finance module on AND invoices = 0 AND day ≥ 5 | 60 | Sending your first invoice | q.odontox.io/guides/invoices |
| 10 | first_prescription | Rx module on AND prescriptions = 0 AND day ≥ 6 | 55 | Writing a prescription | q.odontox.io/guides/prescriptions |
| 11 | trial_thank_you | subscription_status flipped to active within 24h | 100 (overrides) | Welcome to OdontoX Pro | q.odontox.io/guides/pro-overview |
Phase 2 — New paid user (Day 0–30 post-conversion)
| # | Key | Trigger | Priority | Subject | Article |
|---|---|---|---|---|---|
| 12 | paid_welcome | 24h after conversion | 100 | What’s next, now that you’re on Pro | q.odontox.io/guides/pro-roadmap |
| 13 | lab_tracking | Lab module on AND lab_cases = 0 | 85 | Lab tracking in OdontoX | q.odontox.io/guides/lab-tracking |
| 14 | insurance_claims | Insurance module on AND claims = 0 | 80 | Submitting insurance claims | q.odontox.io/guides/insurance-claims |
| 15 | ai_insights | AI module on AND no reports viewed | 75 | Meet Ruby — your weekly clinic report | q.odontox.io/guides/ruby-reports |
| 16 | scale_milestone | patients ≥ 100 AND bulk tools never used | 70 | You have 100 patients in OdontoX | q.odontox.io/guides/scale-tips |
| 17 | ipd_module | IPD module on AND ipd_visits = 0 AND day ≥ 14 | 65 | The IPD module: when you need it | q.odontox.io/guides/ipd |
Final copy — all 17 emails
All emails open with the OdontoX logo at the top of the body and sign off— Sarmad, OdontoX. The trailing P.S. line is italic, muted color.
1.
welcome — Subject: Welcome to OdontoX
Hi , Welcome to OdontoX. Over the next few days we’ll send a small number of short emails — one feature at a time, only when there’s something useful to cover. The first step is the foundation: adding a patient. Once a patient record exists, everything else in OdontoX — appointments, charting, prescriptions, invoices — builds from it. The guide walks through it in about a minute. Adding your first patient → Replies to this email come straight to our team. If you get stuck on anything, just hit reply. — Sarmad, OdontoX P.S. Your clinic data is backed up every night at 2am PKT.
2.
first_patient_added — Subject: Your first patient is in
Hi , is in the system. That’s the foundation built — everything else in OdontoX layers on top of a patient record. The next thirty seconds: open the Appointments tab and drag onto a time slot. That’s how bookings work. Booking appointments → — Sarmad, OdontoX P.S. ⌘K opens a search bar from any page in the app. Fastest way around.
3.
first_appointment — Subject: Booking your first appointment
Hi , patients in the system — time to start booking. Appointments work like a calendar you can drag patients onto. Each booking carries a confirmation message, an automatic 24-hour reminder, and a post-visit follow-up — sent over WhatsApp when you’ve connected it. (We’ll cover that one separately.) How appointments work → — Sarmad, OdontoX P.S. The dots on each slot are status: green confirmed, amber tentative, red cancelled.
4.
whatsapp_off — Subject: Connect WhatsApp to cut no-shows by 40%
Hi , WhatsApp is the highest-leverage integration most OdontoX clinics turn on. Once connected, appointment confirmations, 24-hour reminders, and post-visit follow-ups go out automatically from your clinic’s own number. Across clinics we’ve watched configure it, no-shows drop by around 40% in the first month. The WhatsApp module was rebuilt this month — delivery and read receipts now show on every message, and templates render Markdown. Connecting WhatsApp → — Sarmad, OdontoX P.S. Patients see “via WhatsApp Business,” not “via OdontoX.” Your clinic stays the face.
5.
dental_charting — Subject: Using the dental chart
Hi , The dental chart is one of the more visual parts of OdontoX. Open any patient, switch to the Chart tab, select a tooth, mark a condition, save. Anything you record flows into the treatment plan, prescription, and invoice automatically — you never enter the same data twice. How the chart works → — Sarmad, OdontoX P.S. Adult, child, and mixed dentition layouts switch automatically based on the patient’s age.
6.
clinical_notes — Subject: Clinical notes, the fast way
Hi , Most clinics dread writing notes. OdontoX has a SOAP-style template that autosaves, voice-to-text on mobile, and templates you can reuse across patients. From the appointment screen, click “Add note” — two clicks, then dictate. Writing clinical notes → — Sarmad, OdontoX P.S. Voice transcription works in Urdu and English.
7.
invite_staff — Subject: Add your team to OdontoX
Hi , You’ve been driving OdontoX solo so far. The app works better with the whole clinic on it: receptionists handle bookings and reminders, doctors stay in clinical mode, you get a real audit trail of who did what. Each role sees only what it should — receptionists don’t see clinical notes, doctors don’t see the finance ledger. Inviting staff → — Sarmad, OdontoX P.S. Adding a teammate takes thirty seconds and doesn’t change your billing during the trial.
8.
mobile_app — Subject: OdontoX on iOS and Android
Hi , The mobile app does the things you’d actually want to do on a phone: pull up a patient between cases, check tomorrow’s schedule, reply to a WhatsApp thread, write a quick note. It’s a real native app, not a webview wrapper. Getting the mobile app → — Sarmad, OdontoX P.S. iOS first, Android a few weeks behind on a couple of features. Both work today.
9.
first_invoice — Subject: Sending your first invoice
Hi , The Finance module turns a treatment plan into an invoice in two clicks. PKR or USD, WhatsApp the PDF or share a payment link. Receipts and installment plans work the same way — every transaction stays linked to the patient’s record. Creating invoices → — Sarmad, OdontoX P.S. Year-to-date revenue, by doctor, by service — Reports → Finance.
10.
first_prescription — Subject: Writing a prescription
Hi , Prescriptions in OdontoX use a pre-loaded medicine library that covers local brand and generic names. Type two letters, the drug autocompletes with the standard dosage. Add it to the patient and the PDF is ready to print or WhatsApp. Writing prescriptions → — Sarmad, OdontoX P.S. Your custom dosing presets persist across patients.
11.
trial_thank_you — Subject: Welcome to OdontoX Pro
Hi , You upgraded to Pro today — thank you. That means a great deal to a small team. We’ll keep writing every so often to walk you through the more advanced parts of OdontoX as they become relevant — lab tracking, insurance claims, Ruby Reports, and so on. You can adjust which emails you receive any time in Settings. What’s included with Pro → — Sarmad, OdontoX P.S. Welcome.
12.
paid_welcome — Subject: What’s next, now that you’re on Pro
Hi , Now that the basics are running, the advanced parts of OdontoX become relevant — lab tracking, insurance claims, Ruby Reports, inventory, and a few more. We’ll send a short email when each of those is worth a look. You can also browse them yourself. The Pro roadmap → — Sarmad, OdontoX P.S. Pro pricing locks in for as long as your subscription stays active.
13.
lab_tracking — Subject: Lab tracking in OdontoX
Hi , Lab cases get tracked end-to-end in OdontoX — sent date, expected return, actual return, attached prosthetic photos, patient linkage. No more WhatsApp threads with the lab to keep track of crowns. Tracking lab cases → — Sarmad, OdontoX P.S. Overdue cases get flagged on the dashboard. You’ll never have to chase a lab again.
14.
insurance_claims — Subject: Submitting insurance claims
Hi , Insurance claims in OdontoX pull from the treatment plan automatically — no re-typing procedures, codes, or costs. Generate the claim form, attach photos and notes, send to the insurer. The status tracker shows when claims are submitted, in review, approved, or denied. Submitting claims → — Sarmad, OdontoX P.S. Common insurer templates are pre-loaded. Add your own from Settings → Insurance.
15.
ai_insights — Subject: Meet Ruby — your weekly clinic report (logo: ui/public/ruby.png)
Hi , Ruby is the part of OdontoX we don’t talk about enough. She reviews your clinic’s data and writes a short report every Monday morning — overdue patients, slipping treatments, days you’re losing revenue to no-shows. Ten seconds to read, one click to act on. How Ruby Reports work → — Sarmad, OdontoX P.S. Ruby’s report is written, not generated. We were careful about the tone.
16.
scale_milestone — Subject: You have 100 patients in OdontoX
Hi , Crossing 100 patients changes how you’ll use OdontoX. A few features worth knowing:Features for scale → — Sarmad, OdontoX P.S. We’ve seen clinics break 1,000 patients without performance issues. Don’t worry about the ceiling.
- Bulk import and export from Settings → Data
- Patient tags and segments for recall campaigns
- Saved views in the appointments calendar
17.
ipd_module — Subject: The IPD module: when you need it
Hi , In-Patient Department is for clinics that admit patients overnight — common in larger maxillofacial and surgical practices. If you handle admissions, ward rounds, and discharge summaries, the IPD module turns those into structured records that link back to the patient and the billing. Using IPD → — Sarmad, OdontoX P.S. Most clinics don’t need this. If you do, it’ll save you a notebook.
5. The sending machine
Schedule
A Cloudflare Workers cron runs at 09:00 PKT daily (= 04:00 UTC). Wired intoodontoX/server/src/scheduled.ts alongside the existing trial-expiry job.
Eligibility query
SELECT * FROM users WHERE clinic_id = $1 AND role = 'admin' AND deleted_at IS NULL) and run the picker.
pickLesson(clinic, signals, log) algorithm
currentPhase(clinic): 'trial' if subscription_status = 'trial', otherwise 'paid'.
Activation signals (cheap aggregate queries)
Computed once per clinic per cron run:getActivationSignals(clinicId) function in odontoX/server/src/lib/campaign-signals.ts — one batched query per clinic.
Guardrails (no exceptions)
- One marketing email per clinic per day, maximum. Even if 5 triggers fire, send only the top-priority one.
- Skip Friday + Sunday in PKT. Clinics are typically closed; emails get buried.
- Skip if
lastAppActivityAtis within the last 2 hours. Don’t interrupt an active session. - Skip the day a transactional email is firing to the same clinic (existing trial-expiring, trial-extended, subscription-ended, payment-failed jobs). Coordinated via a shared “scheduled email queue” check before send.
- Skip if the recipient user has
email_marketing_opt_out = true(added as a column tousers— separate from the clinic-level toggle). - Skip if
is_test_account = true(covered in eligibility query — redundant defense). - Skip if the superadmin disabled the campaign for this clinic.
- Dedupe via
clinic_campaign_log(clinic_id, campaign_key)UNIQUE constraint — a key never sends twice to the same clinic.
6. Data model
Columns added to clinics
Column added to users
New table clinic_campaign_log
UNIQUE(clinic_id, campaign_key) constraint is the dedupe guarantee — even with a race on the cron, a lesson never sends twice.
7. Superadmin per-clinic toggle
New section on the existing superadmin clinic detail page (/superadmin/clinics/[id]):
marketing_campaign_disabled_reason along with the actor (marketing_campaign_disabled_by) and timestamp. Toggling on clears those fields.
When toggle is off, the row above the divider expands to show: “Disabled by on . Reason: .“
8. Tracking & success metrics
Per-email instrumentation
- Open — 1×1 transparent pixel at
GET /api/email/pixel?log=<log_id>→ setsclinic_campaign_log.opened_at = NOW()(first hit only). - Click — CTA wrapped through
GET /api/email/click?log=<log_id>&to=<encoded_url>→ setsclicked_at, 302 to the real URL. The real URL is always aq.odontox.ioarticle. - Reply — ZeptoMail inbound webhook to
/api/email/inboundmatches replies byIn-Reply-Toheader to thezepto_message_idwe stored at send → setsreplied_at.
Superadmin dashboard view
A simple admin-only page at/superadmin/campaigns:
- Funnel per
campaign_key: sent → opened → clicked → replied. - Trial-to-paid conversion lift: clinics with campaign ON vs. OFF, controlling for signup cohort week.
- Top 3 most-replied-to lessons (these are the highest-signal copy — what dentists actually engage with).
- Per-clinic timeline of campaign emails (also shown inline on the clinic detail page).
9. Build order
Phase 1 — Foundation (1 week)
- Drizzle migration:
clinicscolumns,users.email_marketing_opt_out,clinic_campaign_logtable. getActivationSignals(clinicId)helper.- Cron skeleton in
scheduled.ts(eligibility query + picker + guardrails, no templates yet). - React Email templates:
welcome,first_patient_added,first_appointment,trial_thank_you(4 of 17). - Superadmin toggle UI (the clinic-detail block + the disable-reason modal).
- Pixel + click tracking endpoints.
Phase 2 — Full library (3–4 days)
- Remaining 13 templates with their triggers.
- Soak test against a staging clinic that has
is_test_account = truebut the campaign manually enabled.
Phase 3 — Analytics dashboard (2 days)
/superadmin/campaignsfunnel and lift view.- Per-clinic timeline drawer on the clinic-detail page.
Phase 4 — Reply tracking (1 day)
- ZeptoMail inbound webhook handler.
In-Reply-Tomatching,replied_atwrite.
10. Rollout gate — shadow week (HARD GATE)
Before the campaign sends to a single real clinic, it runs in shadow mode for 7 days, delivering only to Sarmad’s two inboxes. This is a non-negotiable production gate. Allowlisted recipients during shadow week: Mechanism. New env varCAMPAIGN_ALLOWLIST_EMAILS (comma-separated). When set, the cron’s eligibility step adds a final filter: only send to clinics that have at least one admin whose email matches the allowlist. Any clinic without a matching admin is skipped silently (no log entry, no error). When the env var is unset or empty, the filter is bypassed and the campaign runs for all eligible clinics.
- Both inboxes receive emails (not spam folder; check Gmail Promotions tab too).
-
email.pnglogo renders in Apple Mail, Gmail web, Gmail iOS, Gmail Android, Outlook. -
ruby.pngrenders correctly in theai_insightsemail (asset must be dropped atui/public/ruby.pngbefore this email can fire). - Footer
logo.pngrenders. - Every CTA lands on a real
q.odontox.ioarticle — any 404 is a blocker. - Pixel tracking writes
opened_atafter the email is opened. - Click tracking writes
clicked_atand 302s to the article. - Reply to one email —
replied_atlands within 5 minutes. - Friday + Sunday PKT skip is observed (no sends those days).
- Active-session-skip is observed (open the app on Sarmad’s test clinic, no email arrives that day).
- Transactional-overlap-skip is observed (force a
TrialExpiringEmailday, no marketing email arrives). - Superadmin toggle works — disabling the campaign for the test clinic stops sends within the same day.
- At least 4 distinct lesson keys fire across the 7-day window.
is_test_account = true but with a special override that lets them through the eligibility filter (a force_marketing_campaign = true column, or simpler — flip is_test_account = false on these two specifically). Each clinic has Sarmad as the admin with one of the two allowlisted emails. Both clinics will be deleted or re-flagged after shadow week completes.
11. Out of scope (explicit YAGNI)
- A/B testing. Fixed copy v1; iterate based on reply content. OdontoX’s current trial volume is too small for meaningful A/B significance.
- Urdu localization. English-only v1. Reassess once we hit ≥100 trials/month and have data on Urdu lift.
- Re-engagement of churned clinics. Separate problem, separate spec.
- Power-tip weekly newsletter past day 30 post-Pro. Tempting scope creep; cut.
- Per-user (not per-clinic) campaign log. All admins in a clinic share the same dedupe state — sending one of them an email “counts” for the clinic. Simpler, and matches the “clinic-level onboarding” mental model.
- Trial-expiry / billing emails. These already exist (
TrialExpiringEmail,TrialExtensionEmail,SubscriptionEndedEmail,PaymentFailedEmail) and continue to fire from the existing scheduled job. The marketing cron is coordinated to skip days when these fire (guardrail #4). - Weekly digests, monthly recap, NPS surveys. Future scope.
12. Open questions / decisions to revisit
- Sender name format. Currently
Sarmad <[email protected]>. Should we useSarmad from OdontoXto give the brand a presence in the From column even before the email opens? Defer; A/B test once volume justifies. - Day of cron run. Currently 09:00 PKT. May want to test 08:00 (catch the pre-clinic-opening read) vs. 14:00 (after lunch). Defer.
q.odontox.ioarticle slug naming. Spec uses/guides/<slug>— confirm this matches the existing q.odontox.io URL structure or adjust globally.ruby.pngasset. Needs to exist atodontoX/ui/public/ruby.pngbefore theai_insightsemail ships. Same path convention asemail.png.
Approval
- Voice & visual design — approved (revised v2 after pulling back forced-casual founder voice; logo is
ui/public/email.png, footer logo isui/public/logo.png). - Lesson library — 17 emails locked, expiration/billing excluded.
- Superadmin per-clinic toggle requirement — confirmed.
- Admin-role-only delivery — confirmed.
- Every CTA links to a
q.odontox.ioarticle — confirmed. - Implementation plan — pending (next:
writing-plansskill).

