The question behind the email campaign was simple:
If the product is built around repeated practice of specific grammar sections, what is the lightest reminder system that helps learners return to the right section at roughly the right time?
That question matters because the product is organised around narrow grammar sections rather than broad lessons.
A learner does not just "practice German." They practice things like:
- adjective declension after articles,
- verbs with fixed prepositions,
- sentence connectors in B2 texts,
- nominalisation in C1 written German.
That changes the retention logic.
The campaign system is section-based. Every completion event can create or update one schedule for one user and one grammar section.
That gives the system something concrete to work with:
- what the learner practiced,
- when they practiced it,
- when it should next be revisited,
- and which link should take them directly back there.
The schedule is deliberately simple
The first version uses a fixed four-step review sequence.
| Step | Delay | Purpose |
|---|---|---|
| 0 | +1 day | Catch the first forgetting drop |
| 1 | +3 days | Reinforce while the pattern is still accessible |
| 2 | +7 days | Move into a weekly review rhythm |
| 3 | +14 days | Test whether the pattern is becoming durable |
This is not a full adaptive spaced-repetition system.
It is a practical review schedule for a lightweight email layer.
That trade-off was intentional. A fixed schedule is easier to reason about, easier to debug, and easier to connect to concrete product actions. It also matches the current maturity of the product better than pretending to have a mastery model where the evidence is still thin.
The key detail is that the schedule is resettable.
If a learner practices the same section again before the schedule finishes, the system does not stack reminders. It updates the existing schedule instead. The most recent practice event becomes the new anchor.
That logic lives in a simple upsert:
INSERT INTO campaign_schedule (
user_id,
grammar_section_id,
step,
next_send_at,
preferred_send_hour,
timezone,
status
)
VALUES (...)
ON CONFLICT (user_id, grammar_section_id)
DO UPDATE SET
step = 0,
next_send_at = EXCLUDED.next_send_at,
preferred_send_hour = EXCLUDED.preferred_send_hour,
timezone = EXCLUDED.timezone,
status = 'active';
That turns the schedule into a small state machine rather than a growing pile of pending emails.
Timezone awareness matters more than template polish
A reminder email sent at the wrong local hour is mostly noise.
The system stores the learner’s local send preference implicitly: when the schedule is created, it records the local hour and timezone. The hourly processor then checks whether a schedule is due and whether the current hour in that timezone matches the preferred hour.
Conceptually, the eligibility logic looks like this:
WHERE cs.next_send_at <= NOW()
AND (
cs.preferred_send_hour IS NULL
OR cs.preferred_send_hour = EXTRACT(
HOUR FROM NOW() AT TIME ZONE COALESCE(cs.timezone, 'Europe/Berlin')
)::integer
)
This solves a mundane but important product issue.
A reminder that is technically on time in UTC can still be mistimed in the learner’s actual day. The email does not need perfect personalisation. It does need temporal plausibility.
Most of the work is in the guardrails
The value of the system is not just in sending messages.
It is in not sending the wrong ones.
Before a message goes out, the processor applies several checks:
- subscription state — do not send if the learner unsubscribed,
- pause state — do not send during a pause window,
- already-practiced check — cancel the reminder if the learner already returned to that section,
- weekly cap — do not exceed the configured email frequency,
- comeback mode — reduce cadence for learners who have been inactive longer.
The third one is especially important.
Without it, the system can produce the most irritating kind of reminder: an email asking a learner to review a section they already revisited on their own.
From a systems perspective, that is a small state mismatch. From a product perspective, it makes the platform look inattentive.
That is why the campaign processor is better understood as a decision layer than as a mail sender.
The funnel only became useful when it was broken into stages
The tracking model uses four states:
- sent
- clicked
- exercise started
- exercise completed
The reason to track all four is simple.
"Email worked" is too coarse to guide product changes.
A click without a start suggests a landing or routing problem. A start without a completion suggests exercise friction or session design problems. A low click rate suggests a subject-line or relevance problem.
Once the funnel was staged, it became much easier to tell where the problem actually was.
That is the point where email analytics stopped being vanity metrics and started behaving like product diagnostics.
The useful decision was section-specific, not generic
This is the broader lesson.
The product does not need generic "come back to the app" messages. It needs reminders tied to the learner’s actual practice path.
That is why the schedule is attached to grammar sections rather than to abstract activity streaks.
A reminder that says, in effect, "come back to this exact grammar area you practiced three days ago" is much closer to the product’s logic than a generic engagement email.
The system becomes much lighter and much more relevant at the same time.
Why this worked better as a learning system than as marketing
Thinking of the campaign as a marketing feature would have produced the wrong design.
The important questions are not:
- how many emails were sent,
- how polished the template looks,
- or how often users were nudged.
The important questions are:
- was the reminder tied to the right section,
- did it arrive at a plausible time,
- did it respect the learner’s recent activity,
- and where exactly did the learner drop after interacting with it?
That is learning-system logic, not marketing logic.
The practical lesson
A lightweight email layer can be useful.
But it becomes much more useful when it is tied to:
- explicit section state,
- a resettable schedule,
- good guardrails,
- timezone awareness,
- and a funnel that distinguishes between click, start, and completion.
In that form, the email campaign stops being a generic retention add-on.
It becomes an extension of the product’s core promise: repeated practice of specific grammar areas at the right moment.
