This is the end-to-end recipe for adding or editing an email in the V2 program. It is written so an outside developer or designer can ship a change without reverse-engineering the system. The Atlas (this site) is the _what & how_; supabase/functions/_shared/email-templates/AGENTS.md is the deeper _why_.
Every email is a pure function that returns an HTML string. There is no database call, no network, no framework inside a builder — it takes plain inputs and returns markup. That purity is why the Atlas can render all 100+ emails offline and why they are easy to review.
A finished email is three things:
build<Name>Body(input) in a routes/*-bodies.ts file, composed fromthe shared components (see Components) using the design tokens (see Tokens);
baseEmailTemplate({ audience, mailClass, ... }) wraps the body in themasthead + footer for that audience × mail-class (see the Chrome map);
scripts/audit/email-catalog.ts so it shows up here.This single choice sets the sender, the footer, and the legal payload. Pick from the Chrome map:
member (a candidate), partner (a hiring company), or admin (our concierge/ops team).marketing, transactional, security, service, system, alert, or digest.Rule of thumb: only marketing mail carries an unsubscribe (RFC 8058). Everything else must not — putting marketing chrome on transactional mail reclassifies it and pollutes the stream.
A real lifecycle event usually notifies more than one party. The single source of truth is RECIPIENT_MAP in supabase/functions/_shared/email-recipient-map.ts — look up the event and you will see every recipient (candidate, partner, interviewer, strategist, admin) with its own audience × mail-class. If your email is a new lifecycle event, add a row there first.
Concierge mirrors always route to the assigned strategist (primary) plus the team inbox (CC).
Add a pure build<Name>Body(input): string to the matching file under supabase/functions/notification-dispatcher/routes/ (e.g. candidate-application-bodies.ts, partner-lifecycle-bodies.ts, concierge-bodies.ts). Compose it from the components — do not hand-roll table HTML. Keep it pure: the AI insight line and any data are passed _in_ as plain arguments.
Honour the house rules:
MetricStat, BarChart, Sparkline) — never inline <svg>.Add one item to the right cluster in scripts/audit/email-catalog.ts:
file — the render slug, e.g. cn-offer-declined (matches audit-artifacts/<file>.html).name — the display name.event — the RECIPIENT_MAP key, when there is one (this resolves the recipients shown here).trigger — a human-readable fire condition, when there is no single event.score — leave unset until the email has passed the 98-audit.The group already carries the defaults (audience, mail-class, status, builder file); only set an item-level override when the email differs from its cluster.
Build the whole Atlas with one command, then open it on the local server:
deno run -A scripts/audit/build-atlas.tspython3 -m http.server 8899 --directory audit-artifacts then open http://localhost:8899/Your email appears on the Atlas and Index, and gets its own spec page with metadata, the HTML source (copy / download for Litmus), and a notes box. Check it at desktop and mobile.
deno check the builder file and the catalog.deno test -A scripts/audit/email-catalog.test.ts — guards the catalog against RECIPIENT_MAP drift.score.On any spec page you can write a note (saved in your browser) and export it, or click Open GitHub issue to file it against the repo with the email pre-filled. Nothing here is wired to a live handler or deployed — the Atlas is reviewable bodies and galleries only. Deploys happen separately and only on an explicit owner go-ahead.