Skip to main content

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 a q.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 = false
  • clinics.marketing_campaign_enabled = true (new column — superadmin per-clinic toggle, defaults true)
  • clinics.marketing_unsubscribed = false (new column — clinic-side opt-out, defaults false)
  • One of:
    • subscription_status = 'trial' AND trial_end_date > NOW(), or
    • subscription_status = 'active' AND activated_at > NOW() - INTERVAL '30 days'
Recipient = every user in the clinic with 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) uses ui/public/ruby.png at 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 #111 on #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

VariableSource
{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)

#KeyTriggerPrioritySubjectArticle
1welcomeSignup confirmed AND patients = 0100Welcome to OdontoXq.odontox.io/guides/welcome
2first_patient_addedpatients ≥ 1, sent next morning95Your first patient is inq.odontox.io/guides/appointments
3first_appointmentpatients ≥ 3 AND appointments = 090Booking your first appointmentq.odontox.io/guides/appointments
4whatsapp_offDay ≥ 3 AND WhatsApp module off AND patients ≥ 585Connect WhatsApp to cut no-shows by 40%q.odontox.io/guides/whatsapp-setup
5dental_chartingappointments ≥ 1 AND chart_entries = 080Using the dental chartq.odontox.io/guides/dental-chart
6clinical_notesappointments ≥ 1 AND clinical_notes = 0 AND day ≥ 475Clinical notes, the fast wayq.odontox.io/guides/clinical-notes
7invite_staffuser_count = 1 AND day ≥ 570Add your team to OdontoXq.odontox.io/guides/invite-staff
8mobile_appNo mobile session ever AND day ≥ 665OdontoX on iOS and Androidq.odontox.io/guides/mobile-app
9first_invoiceFinance module on AND invoices = 0 AND day ≥ 560Sending your first invoiceq.odontox.io/guides/invoices
10first_prescriptionRx module on AND prescriptions = 0 AND day ≥ 655Writing a prescriptionq.odontox.io/guides/prescriptions
11trial_thank_yousubscription_status flipped to active within 24h100 (overrides)Welcome to OdontoX Proq.odontox.io/guides/pro-overview

Phase 2 — New paid user (Day 0–30 post-conversion)

#KeyTriggerPrioritySubjectArticle
12paid_welcome24h after conversion100What’s next, now that you’re on Proq.odontox.io/guides/pro-roadmap
13lab_trackingLab module on AND lab_cases = 085Lab tracking in OdontoXq.odontox.io/guides/lab-tracking
14insurance_claimsInsurance module on AND claims = 080Submitting insurance claimsq.odontox.io/guides/insurance-claims
15ai_insightsAI module on AND no reports viewed75Meet Ruby — your weekly clinic reportq.odontox.io/guides/ruby-reports
16scale_milestonepatients ≥ 100 AND bulk tools never used70You have 100 patients in OdontoXq.odontox.io/guides/scale-tips
17ipd_moduleIPD module on AND ipd_visits = 0 AND day ≥ 1465The IPD module: when you need itq.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:
  • Bulk import and export from Settings → Data
  • Patient tags and segments for recall campaigns
  • Saved views in the appointments calendar
Features for scale → — Sarmad, OdontoX P.S. We’ve seen clinics break 1,000 patients without performance issues. Don’t worry about the ceiling.

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 into odontoX/server/src/scheduled.ts alongside the existing trial-expiry job.

Eligibility query

SELECT c.id, c.name, c.subscription_status, c.trial_end_date, c.activated_at
FROM clinics c
WHERE c.is_test_account = false
  AND c.marketing_campaign_enabled = true
  AND c.marketing_unsubscribed = false
  AND (
    (c.subscription_status = 'trial' AND c.trial_end_date > NOW())
    OR
    (c.subscription_status = 'active' AND c.activated_at > NOW() - INTERVAL '30 days')
  );
For each eligible clinic, fetch admins (SELECT * FROM users WHERE clinic_id = $1 AND role = 'admin' AND deleted_at IS NULL) and run the picker.

pickLesson(clinic, signals, log) algorithm

// Sort lessons by priority desc; trial_thank_you and paid_welcome override priority 100
const candidates = LESSONS
  .filter(l => l.phase === currentPhase(clinic))
  .sort((a, b) => b.priority - a.priority);

for (const lesson of candidates) {
  if (log.has(lesson.key)) continue;            // already sent — dedupe
  if (!lesson.trigger(signals, clinic)) continue; // signal not met
  return lesson;                                 // top eligible lesson wins
}
return null;                                     // nothing fires today
currentPhase(clinic): 'trial' if subscription_status = 'trial', otherwise 'paid'.

Activation signals (cheap aggregate queries)

Computed once per clinic per cron run:
type Signals = {
  patients: number;
  appointments: number;
  chartEntries: number;
  clinicalNotes: number;
  invoices: number;
  prescriptions: number;
  labCases: number;
  insuranceClaims: number;
  ipdVisits: number;
  aiReportsViewed: number;
  userCount: number;
  whatsappEnabled: boolean;
  financeEnabled: boolean;
  rxEnabled: boolean;
  labEnabled: boolean;
  insuranceEnabled: boolean;
  aiEnabled: boolean;
  ipdEnabled: boolean;
  mobileSessions: number;        // distinct mobile sessions ever
  bulkToolsUsed: boolean;        // bulk import OR export ever
  lastAppActivityAt: Date | null;
  dayInTrial: number;            // floor((NOW - createdAt) / 1 day)
  dayInPaid: number;             // floor((NOW - activatedAt) / 1 day)
};
Implemented as a single getActivationSignals(clinicId) function in odontoX/server/src/lib/campaign-signals.ts — one batched query per clinic.

Guardrails (no exceptions)

  1. One marketing email per clinic per day, maximum. Even if 5 triggers fire, send only the top-priority one.
  2. Skip Friday + Sunday in PKT. Clinics are typically closed; emails get buried.
  3. Skip if lastAppActivityAt is within the last 2 hours. Don’t interrupt an active session.
  4. 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.
  5. Skip if the recipient user has email_marketing_opt_out = true (added as a column to users — separate from the clinic-level toggle).
  6. Skip if is_test_account = true (covered in eligibility query — redundant defense).
  7. Skip if the superadmin disabled the campaign for this clinic.
  8. 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

ALTER TABLE clinics
  ADD COLUMN marketing_campaign_enabled BOOLEAN NOT NULL DEFAULT TRUE,
  ADD COLUMN marketing_unsubscribed BOOLEAN NOT NULL DEFAULT FALSE,
  ADD COLUMN marketing_campaign_disabled_by UUID REFERENCES users(id),
  ADD COLUMN marketing_campaign_disabled_at TIMESTAMPTZ,
  ADD COLUMN marketing_campaign_disabled_reason TEXT,
  ADD COLUMN activated_at TIMESTAMPTZ;   -- if not already present; flips on trial → paid

Column added to users

ALTER TABLE users
  ADD COLUMN email_marketing_opt_out BOOLEAN NOT NULL DEFAULT FALSE;

New table clinic_campaign_log

CREATE TABLE clinic_campaign_log (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  clinic_id         UUID NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
  user_id           UUID NOT NULL REFERENCES users(id),
  campaign_key      TEXT NOT NULL,
  subject           TEXT NOT NULL,
  sent_at           TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  opened_at         TIMESTAMPTZ,
  clicked_at        TIMESTAMPTZ,
  replied_at        TIMESTAMPTZ,
  zepto_message_id  TEXT,
  UNIQUE (clinic_id, campaign_key)
);

CREATE INDEX idx_campaign_log_clinic ON clinic_campaign_log(clinic_id);
CREATE INDEX idx_campaign_log_key ON clinic_campaign_log(campaign_key);
CREATE INDEX idx_campaign_log_sent ON clinic_campaign_log(sent_at DESC);
The 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]):
┌─ Onboarding emails ────────────────────────────────────────────┐
│                                                                 │
│  [●———○] Send onboarding campaign           ENABLED             │
│                                                                 │
│  ─────────────────────────────────────────                      │
│                                                                 │
│  Last 5 emails sent to this clinic:                             │
│                                                                 │
│   2026-05-13  welcome                opened · clicked           │
│   2026-05-14  first_patient_added    opened                     │
│   2026-05-15  first_appointment      —                          │
│                                                                 │
│  Total: 3 sent · 67% open · 33% click · 0 replies               │
└─────────────────────────────────────────────────────────────────┘
When toggling off, a modal asks for a free-text reason (required) — captured into 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> → sets clinic_campaign_log.opened_at = NOW() (first hit only).
  • Click — CTA wrapped through GET /api/email/click?log=<log_id>&to=<encoded_url> → sets clicked_at, 302 to the real URL. The real URL is always a q.odontox.io article.
  • Reply — ZeptoMail inbound webhook to /api/email/inbound matches replies by In-Reply-To header to the zepto_message_id we stored at send → sets replied_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: clinics columns, users.email_marketing_opt_out, clinic_campaign_log table.
  • 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 = true but the campaign manually enabled.

Phase 3 — Analytics dashboard (2 days)

  • /superadmin/campaigns funnel and lift view.
  • Per-clinic timeline drawer on the clinic-detail page.

Phase 4 — Reply tracking (1 day)

  • ZeptoMail inbound webhook handler.
  • In-Reply-To matching, replied_at write.

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 var CAMPAIGN_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.
// inside campaign-runner.ts, after standard eligibility filter
const allowlist = (env.CAMPAIGN_ALLOWLIST_EMAILS || '').split(',').map(s => s.trim()).filter(Boolean);
if (allowlist.length > 0) {
  eligibleClinics = eligibleClinics.filter(c =>
    c.admins.some(a => allowlist.includes(a.email.toLowerCase()))
  );
}
To enable shadow mode: Set the env var in Cloudflare Workers production secrets. To go live to all clinics: Remove the env var (or set to empty string) and redeploy. Shadow week verification checklist — Sarmad signs off before flipping live:
  • Both inboxes receive emails (not spam folder; check Gmail Promotions tab too).
  • email.png logo renders in Apple Mail, Gmail web, Gmail iOS, Gmail Android, Outlook.
  • ruby.png renders correctly in the ai_insights email (asset must be dropped at ui/public/ruby.png before this email can fire).
  • Footer logo.png renders.
  • Every CTA lands on a real q.odontox.io article — any 404 is a blocker.
  • Pixel tracking writes opened_at after the email is opened.
  • Click tracking writes clicked_at and 302s to the article.
  • Reply to one email — replied_at lands 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 TrialExpiringEmail day, 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.
Once all boxes are checked, the env var is removed, Sarmad confirms, and the campaign rolls out to all eligible non-test clinics on the next 09:00 PKT cron run. Test clinic setup for the shadow week. Two test clinics will be created with 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 use Sarmad from OdontoX to 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.io article slug naming. Spec uses /guides/<slug> — confirm this matches the existing q.odontox.io URL structure or adjust globally.
  • ruby.png asset. Needs to exist at odontoX/ui/public/ruby.png before the ai_insights email ships. Same path convention as email.png.

Approval

  • Voice & visual design — approved (revised v2 after pulling back forced-casual founder voice; logo is ui/public/email.png, footer logo is ui/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.io article — confirmed.
  • Implementation plan — pending (next: writing-plans skill).