Captured live from https://api.resend.com using the key in .env.local (RESEND_API_KEY) on 2026-08-13. Automations and templates ARE queryable and creatable via the Resend API (GET/POST /automations, GET/POST /templates, POST /templates/{id}/publish) — this page is proof, and Gap 4 below was closed the same way. Re-run the calls below to refresh; nothing here is hand-maintained. Resend has no staging environment — this is production, so POST /events/send sends a real email.

Design at a glance

Custom events (7)

Event names are namespaced codex: — Resend reserves the resend: prefix for its own system events. Only one carries a declared schema. All product-milestone sends (everything except signup and billing) go to every domain_owner of the affected org, resolved via membersuser_profiles. Every sender is best-effort: a missing RESEND_API_KEY or a Resend outage is swallowed (.catch() at the call site) and never fails the request that triggered it.

Automations (7)

Each automation triggers on event_received for exactly one event above.

New User Signup — the full free-tier drip (completed 2026-08-13)

All six free-signup-* templates are now wired in, end to end. Timing changed too — the gap before the second email dropped from the original 1 week to 1 day.
As-built observation, not a defect flag: free-signup-02-belief-breaker is referenced by two separate send_email steps (day 1 and day 4) — every other template in the sequence is sent exactly once. This may be a deliberate second touch on the same asset, or an artifact of assembling the sequence in the Resend dashboard. Worth a glance next time the sequence is edited, not urgent.

Subscription Plan Changed — the only branching automation

Three static templates, one per plan — not one dynamic template driven by a PLAN_NAME variable, even though the code computes plan_name for the payload. See Gap 3.

Subscription Cancelled — added 2026-08-13

Deliberately plan-agnostic copy — it does not reference event.plan_name. Given Gap 2’s unresolved question about whether Resend auto-binds event-payload keys to template variables, this automation was built to not depend on the answer either way.

Templates (14)

All published, dark-themed (#070c1c ink / #02c39a teal — the product’s own brand tokens), sender PolicyCodex <info@mailer.policycodex.cloud>.

Segments / Audiences (1)

“General” (6decbfb4-997b-45b3-b984-cc89246f0db5). Resend exposes the same contact list under two API paths — GET /audiences/{id}/contacts and GET /segments/{id}/contacts return identical data; this is one resource with a naming transition in Resend’s own API, not two lists to keep in sync. Membership is added only by the New User Signup automation’s add_to_segment step; nothing in this codebase removes a contact from it (cancellation only sets unsubscribed: true via the Contacts API, which stops sends without leaving the segment).

Webhooks (1)

Signed with Svix, verified against RESEND_WEBHOOK_SIGNING_SECRET (distinct from Clerk’s signing secrets). See Growth data flows for how events become email_sends/email_events rows.

API keys (4, Resend’s own — not this repo’s secrets)

Findings for engineering

These are observations from the live account, not confirmed bugs — flagged here so engineering can decide, not fixed by this documentation pass.

Gap 1: RECIPIENT_FIRST_NAME is very likely never populated

Two call sites send a lowercase snake_case payload key, first_name: clerk-to-resend’s handleUserCreated and github-scan’s notifyGithubMilestones (first-scan branch). The templates that want a first name declare the variable as RECIPIENT_FIRST_NAME (free-signup-01-love-letter, event-first-full-scan). Neither automation step maps event.first_name → RECIPIENT_FIRST_NAME explicitly (the one place this codebase does map variables explicitly is New User Signup’s second send_email step, for APP_URL/DASHBOARD_URL). Unless Resend does case/format- insensitive auto-binding from event payload keys to variable names, every send of these two templates falls back to the literal string “there” — never a name. Confirm behaviour with a live probe (see below) before treating this as certain.

Gap 2: plan_name payload field is unused

clerk-to-resend computes a display name (planDisplayName(planSlug)) and sends it as plan_name in the codex:subscription_plan_changed payload. But the automation branches into three static templates (one hardcoded name per plan in the HTML itself), none of which declares a PLAN_NAME variable. The computed value is currently dead code from Resend’s perspective — either the templates were meant to merge, or the branch-to-static-template design superseded it and plan_name can be dropped from the payload.

Operational note: demo provisioning writes into the live segment

pnpm demo:provision creates real Clerk users for the ACME demo estate (owner+clerk_test@acme.example.com etc.), which fire real user.created webhooks, which flow through this exact pipeline into the live “General” segment — confirmed live: all three ACME demo addresses are in it today. Harmless (synthetic addresses, Clerk test-mode) but worth knowing before reading segment size as a real-lead count.

Reproducing this audit

GET /automations and GET /templates are read-only inventory calls, safe to run any time. POST /events/send sends a real email to the address given — only use your own inbox, never a customer’s.