Docs / Admin
Event email automation and templates
Summary: When someone registers for a consultation, an Inngest workflow sends an instant confirmation email (with an iCal attachment) and schedules 7-day, 1-day, and 1-hour reminders plus a post-event thank-you and follow-up — all time-driven, per-attendee, and toggleable per consultation. Emails are built from a visual block-based template system, and every send is tracked in an Email Log with Resend-driven delivery status.
Purpose: Automate the routine event-communication sequence so staff don't have to manually email every attendee at every stage of a consultation's lifecycle, while keeping full delivery visibility.
Audience: Admin and staff managing consultations and email templates.
Prerequisites:
- A consultation with a
scheduled_atdate and at least one registered attendee with an email address. - Resend configured for outbound send and delivery webhooks (
RESEND_API_KEY).
Overview
Key concepts:
- Event email sequence — a chain of Inngest functions triggered by attendee registration, each firing at a specific offset from the consultation's scheduled time.
- Per-consultation email settings (
consultation_email_settings) — toggles to enable/disable each email type, a pause switch, custom text overrides, logistics fields, and a virtual-meeting URL, scoped to one consultation. - Email template — a named, versioned, block-based HTML template (
email_templatestable) used for the visual builder; separate from the hard-coded HTML renderers that actually generate event emails today. - Email log (
admin/emails) — a record of every email sent through the platform, with delivery status updated by Resend webhooks.
Screenshot: The Email Templates list showing template name, subject, a category badge (Event/Reminder/Follow-up/Notification/Custom), version number, System/Custom badge, and Active/Inactive status.
The event email sequence
Triggered by the consultation/attendee.registered event (src/lib/platform/workflows/functions/event-email-confirmation.ts):
- Confirmation (immediate) — sent unless
settings.sendConfirmation === false. Subject:Registration Confirmed — <title>. Includes the consultation title, type, formatted date/time in Philippine Time, a venue block (venue name + Google Maps link + virtual meeting link if set), a logistics block, the agenda grouped by day (if agenda items exist, with a link to view the full agenda online), a check-in QR code block (image + text link, only if the attendee has an ID), and a note that an iCal invite is attached. The.icsfile is generated viagenerateICalStringand attached asinvite.ics. - 7-day reminder — scheduled for
scheduled_at − 7 days, unlesssettings.sendReminder7d === falseor that time has already passed. Subject:Next Week: <title>. Includes the agenda. - 1-day reminder — scheduled for
scheduled_at − 1 day, unlesssettings.sendReminder1d === false. Subject:Tomorrow: <title>. Includes the agenda and the logistics block. - 1-hour reminder — scheduled for
scheduled_at − 1 hour, unlesssettings.sendReminder1h === false. Subject:Starting Soon: <title>. No agenda block (kept short for last-minute delivery). - Thank-you — scheduled for
scheduled_at + 2 hours, unlesssettings.sendThankYou === false. Subject:Thank You — <title>. - Follow-up — scheduled for
scheduled_at + 3 days, unlesssettings.sendFollowUp === false. Subject:Follow-up — <title>. Includes up to 10 action items (fromconsultation_noteswherenote_type = 'action_item') and up to 3 upcoming events. A "View Meeting Minutes" button appears only when aminutesUrlis supplied — the follow-up sender currently passesnullfor this, so the button does not render in practice yet.
Each reminder is dispatched via a separately-scheduled Inngest event (consultation/email.send-reminder for 7d/1d/1h, consultation/email.send-post-event for thank-you/follow-up), fired with a future ts timestamp so Inngest's own scheduler handles the delay — no cron polling loop is involved.
Safety checks before every send
- Reminders (
event-email-reminder.ts): skipped if the attendee'srsvp_statusiscancelled, if the consultation's email settings arepaused, or if that specific reminder type was disabled after scheduling. - Thank-you / follow-up (
event-email-post-event.ts): only sent if the attendee'srsvp_statusisattendedorconfirmed. Also deduplicated — since this trigger can fire from both the time-scheduled path and an on-scan check-in trigger, the function checksemail_logfor an existing non-failedsend of the sameemailTypeto the same attendee before sending again.
Per-consultation controls
consultation_email_settings (one row per consultation, src/lib/modules/consultations/email-settings.ts) holds:
| Field | Purpose |
|---|---|
sendConfirmation, sendReminder7d, sendReminder1d, sendReminder1h, sendThankYou, sendFollowUp | Per-type on/off toggles (default enabled — only an explicit false disables) |
paused | Master kill switch — when true, reminders stop sending even if individually enabled |
logistics | Free-form key/value pairs rendered in the logistics block (parking, dress code, WiFi, emergency contact, etc.) |
virtualMeetingUrl | Overrides the consultation's own virtual meeting link if set |
customConfirmationText, customThankYouText | Override the default confirmation/thank-you body copy |
Updating settings requires the staff role and is logged to the audit trail as consultation.email_settings_updated.
Email template builder
/admin/email-templates lists every template with its subject, category (Event, Reminder, Follow-up, Notification, Custom), version number, a System badge for the 12 seeded defaults, an Active/Inactive badge, block count, usage count, and last-edited date.
The 12 default system templates
Seeded via migration (is_system = true, not deletable through the templates admin page in the current UI): Event Confirmation, 7-Day Reminder, 1-Day Reminder, 1-Hour Reminder, Thank You, Follow-Up, Waitlist Notification, Cancellation Confirmation, Investor Qualification, B2B Meeting Scheduled, Action Item Reminder, Monthly Report Ready. These use variable placeholders like {{event.title}}, {{attendee.name}}, {{count}}, {{period}}, {{report.url}}.
Note: these system templates are a separate, block-based data model from the hard-coded HTML renderers described in the event email sequence above (renderConfirmation, renderReminder, renderThankYou, renderFollowUp in src/lib/platform/email/event-templates.ts). The two are not currently wired together — editing the "7-Day Reminder" template in the builder does not change what event-email-reminder.ts actually sends. Confirm this integration gap before relying on the builder to customize live event emails.
Creating a template (/admin/email-templates/new)
- Enter Template Name and Subject Line (supports
{{variables}}). - Choose a Category: Custom, Event, Reminder, Follow-up, Notification.
- Click Create Template. It's created with a starter block set:
header,greeting("Hi {{attendee.name}},"),text("Enter your email content here..."),footer.
Editing a template
The edit page (/admin/email-templates/<id>) opens the TemplateEditor — a visual block editor with a live preview generated by renderTemplate(blocks). Available block types: header, greeting, text, image, button, divider, spacer, event_card, calendar_buttons, logistics, qr_code, agenda, action_items, next_events, footer.
Templates also carry a version number and usageCount; the page title reads "Visual template editor — drag blocks, edit content, preview output."
Using a template in a campaign
The campaign composer (/communications/campaigns/new) can pull a template into the email content: selecting it from the Template dropdown fetches the template, renders it via renderTemplate(tpl.blocks, {}), and pre-fills the Subject and Body fields (toast: Applied "<name>"). See Running campaigns and building audiences for the full campaign flow.
Delivery tracking (Email Log)
/admin/emails lists recent sends from email_log, with:
- Filters: Status (
sent,delivered,bounced,failed,opened,clicked) and Source (automation,consultations,outputs/Documents). - Columns: To, Subject, Status (color-coded badge), Source, Sent timestamp.
- Failed Email Details section — shown only when at least one logged email has
status = 'failed'and anerrormessage, listing the recipient and error text.
The page notes: "Recent email activity. Delivery status updated via Resend webhooks." — opens, clicks, bounces, and delivery confirmations arrive asynchronously from Resend and update the corresponding email_log row's status after the initial send.
Roles and permissions
| Action | Minimum role |
|---|---|
| View/filter the Email Log | authenticated staff (page has no explicit role gate beyond the authenticated layout) |
| Create/edit email templates | authenticated staff (no distinct admin-only gate found on the template actions) |
| Update per-consultation email settings | staff |
Best practices
Set logistics and virtual meeting details before the first registration. The confirmation email is sent immediately on registration — settings changed afterward won't retroactively update emails already delivered, only subsequent ones (reminders re-read settings at send time).
Use paused for a consultation-wide hold, not individual toggles. If an event is being rescheduled, paused stops all reminders at once without losing the individual on/off configuration.
Don't expect template-builder edits to change live event emails yet. Until the builder and the hard-coded renderers are unified, edit src/lib/platform/email/event-templates.ts directly (a code change) to alter actual event email content.
Warnings
Reminders scheduled in the past are silently skipped. If a consultation is created or an attendee registers less than 7 days (or 1 day / 1 hour) before the event, that reminder is never scheduled — there's no "send immediately" fallback.
Thank-you and follow-up only fire for attended or confirmed attendees. A no_show or registered-but-never-confirmed attendee will not receive either email, even if the time-based trigger fires.
Troubleshooting
An attendee never received their confirmation email
Cause: Either attendeeEmail was empty at registration (the workflow returns { status: "skipped", reason: "no email" }), the consultation has no scheduled_at, or sendConfirmation is disabled/paused in that consultation's email settings.
Fix: Confirm the attendee record has an email, the consultation has a scheduled date, and check /admin/emails filtered by source consultations for a log entry with an error.
A reminder didn't go out
Cause: The attendee's RSVP status is cancelled, the consultation's settings are paused, that specific reminder type was disabled, or the scheduled send time had already passed when the confirmation workflow ran (e.g. event created less than 7 days out).
Fix: Check the consultation's email settings for that reminder type and the paused flag; check the attendee's RSVP status.
Follow-up or thank-you sent twice
Cause: Should not happen — the workflow explicitly checks email_log for a prior non-failed send of the same emailType to the same attendee before sending. If duplicates appear, check whether email_log rows for that send are marked failed (which would allow a legitimate retry).
Fix: Inspect the email_log rows for that attendee/emailType combination in /admin/emails.
Editing a system template in the builder doesn't change what attendees receive
Cause: The block-based email_templates used by the visual builder are not currently connected to the hard-coded renderers (renderConfirmation, renderReminder, etc.) that generate real event emails.
Fix: For now, event email content changes require editing src/lib/platform/email/event-templates.ts directly. Treat the builder's "System" templates as a preview/reference set, not the live source.
FAQ
Can I customize the reminder schedule (e.g. add a 2-day reminder)? No. Only 7-day, 1-day, and 1-hour offsets exist in the current workflow; adding another cadence requires a code change.
Does the iCal invite update if the consultation is rescheduled?
The .ics is generated fresh each time the confirmation email is sent (registration time), using the consultation's scheduled_at and ended_at (or a 2-hour default duration). There is no re-send of a fresh calendar invite triggered automatically by a later reschedule — check for a manual resend action if the date changes after registration.
Are RSVP-based invites (Yes/No buttons) part of this sequence?
A separate template (renderRsvpInvite) exists for pre-registration RSVP invitations with Yes/No confirmation links, distinct from the post-registration confirmation flow above — it is not triggered by the same consultation/attendee.registered event.
Where do I see how many times a template has been used?
The Email Templates list shows a Used <N> times counter per template — but that only reflects manual use in the block-based campaign flow, not automatically-sent event emails (which don't go through this template model).
Related articles
- Running campaigns and building audiences — how templates plug into manual campaign sends and the human-review send gate
- A dedicated guide to per-consultation email settings and check-in QR flow is planned.