{"id":1108,"title":"SOP: Email Send Workflow","slug":"sop-email-send","html_content":"<h2 id=\"sop-email-send-workflow\">SOP: Email Send Workflow</h2>\n<h3 id=\"purpose\">Purpose</h3>\n<p>Standard process for composing and sending branded emails to Westside parents. Used by agents (Ava, Penny, dev agents) and humans (Lucas, Marcus). Applies to all email sends — blasts, transactional, and one-off. Produces a sent email with EmailLog audit trail, or a clear \"not approved\" stop. The key outcome is that new emails can be sent without code changes when the layout and audience query already exist.</p>\n<h3 id=\"steps\">Steps</h3>\n<ol>\n<li><strong>Intake.</strong> Requester (Marcus or Lucas) describes email intent. Composer (agent) determines:\n<ol>\n<li>Layout — <code>notification</code> (info only), <code>action</code> (CTA button), or <code>announcement</code> (multi-section). See <code>arch-email</code> for layout definitions.</li>\n<li>Audience — which query from <code>email_queries.py</code> registry (e.g. <code>unsigned_contracts</code>, <code>incomplete_profiles</code>). If no query exists for this audience, STOP — create a Forgejo issue + board item for the new query first.</li>\n<li>EmailType — which <code>EmailType</code> enum value. If none exists, STOP — create a Forgejo issue + board item for the migration first.</li>\n</ol>\n</li>\n<li><strong>Draft content.</strong> Composer writes the email data dict and presents it to Approver (Lucas) in the conversation. Include: layout, email_type, query, subject (with per-recipient <code>{{placeholders}}</code>), data dict (headline, body, cta_text, cta_url, footer_note). Do NOT call any endpoint yet.</li>\n<li><strong>Approver reviews draft text.</strong> Approver says \"looks good\" or requests changes. If changes requested, revise and re-present. Loop until text is approved.</li>\n<li><strong>Send test email.</strong> Call <code>POST /email/blast</code> with the draft payload plus <code>\"test_email\": \"draneylucas@gmail.com\"</code>. Report: \"Test email sent to draneylucas@gmail.com. Check your phone.\"</li>\n<li><strong>STOP — wait for approval.</strong> Do NOT proceed. Approver checks email on their phone (real Gmail renderer, real mobile viewport). Approver responds:\n<ol>\n<li><strong>\"Approved\"</strong> → proceed to Step 6.</li>\n<li><strong>\"Change X\"</strong> → go back to Step 2, revise content, re-send test. Loop until approved.</li>\n<li><strong>\"Kill it\"</strong> → stop entirely. Do not send.</li>\n</ol>\n</li>\n<li><strong>Confirm blast scope.</strong> Query the audience count: report \"Sending to N parents. Confirm?\" Wait for explicit \"send it\" or \"yes\" from Approver.</li>\n<li><strong>Blast.</strong> Call <code>POST /email/blast</code> without <code>test_email</code> param. Report results: \"Sent N emails. M errors: [details if any].\"</li>\n<li><strong>Verify.</strong> Confirm EmailLog entries via database query or admin endpoint. Report send count matches expected audience.</li>\n</ol>\n<h4 id=\"contract-emails-enhanced-gate\">Contract Emails (Enhanced Gate)</h4>\n<p>Contract-related emails (contract_offer, contract_reminder) require double approval:</p>\n<ol>\n<li>Steps 1-5 as above (first approval after test email on phone).</li>\n<li>Approver says \"approved\" after checking test on phone.</li>\n<li>Show recipient list and count. Approver says \"send it\" (second approval).</li>\n<li>Send ONE real email to a single parent (not test_email — a real recipient). Approver verifies in that parent's Gmail if possible, or confirms intent.</li>\n<li>Approver says \"yes, blast the rest\" (third confirmation). Then blast.</li>\n</ol>\n<h4 id=\"new-email-type-checklist\">New Email Type Checklist</h4>\n<p>If the email needs infrastructure that doesn't exist:</p>\n<ol>\n<li>New audience query needed? → Create Forgejo issue + board item for <code>email_queries.py</code> change.</li>\n<li>New EmailType enum value needed? → Create Forgejo issue + board item for alembic migration.</li>\n<li>New layout needed? → Create Forgejo issue + board item for MJML template + compile.</li>\n<li>All three exist? → No code needed. Proceed directly to Step 1.</li>\n</ol>\n<h3 id=\"rules\">Rules</h3>\n<ul>\n<li><strong>NEVER</strong> send to real recipients without explicit approval in the current conversation. Approval from a previous session does not carry over.</li>\n<li><strong>ONE APPROVAL = ONE SEND.</strong> Every individual send (test or blast) requires its own explicit 'send it' / 'yes' in the conversation immediately before that send. Prior approvals do NOT authorize subsequent sends, even minutes apart. A new tool call = a new send = a new approval required.</li>\n<li><strong>CONTENT CHANGES DO NOT AUTHORIZE A SEND.</strong> 'Change the date to Friday' or 'fix the wording' is instruction to modify the draft, NOT to send it. After applying the change, re-present the draft and wait for explicit send approval. 2026-04-13 incident: one approval was treated as a blanket, resulting in 3 unapproved sends to a real recipient.</li>\n<li><strong>NEVER</strong> skip the test email step. Every email send starts with <code>test_email=draneylucas@gmail.com</code>.</li>\n<li><strong>NEVER</strong> create audience queries or layouts inline during a send workflow. Missing infrastructure = code ticket first.</li>\n<li><strong>NEVER</strong> assume approval. 'Looks good' on the draft text is NOT approval to blast. The phone check (Step 5) is the gate.</li>\n<li><strong>When in doubt, ask.</strong> The cost of asking 'send this now?' is 2 seconds. The cost of an unapproved send is trust erosion and potential inbox contamination for real recipients.</li>\n<li>Contract emails require double approval — see enhanced gate above.</li>\n<li>Test email address: <code>draneylucas@gmail.com</code> (Lucas's personal Gmail for mobile verification). For Marcus-facing tests, use <code>marcusdraney23@gmail.com</code> ONLY with explicit approval for each send.</li>\n<li>The browser is not the truth. Gmail's renderer is proprietary. Only real emails on real phones count as verification.</li>\n<li>EmailLog is automatic — do not skip or manually log. Every <code>send_templated_email()</code> call writes to <code>email_log</code>.</li>\n</ul>\n<h3 id=\"related\">Related</h3>\n<ul>\n<li><code>arch-email</code> — architecture reference: components, layouts, preview workflow, design decisions</li>\n<li><code>project-westside-basketball</code> — parent project</li>\n<li><code>sop-board-workflow</code> — board workflow for creating tickets when infrastructure is missing</li>\n</ul>","is_public":true,"note_type":"sop","status":"active","parent_slug":null,"position":null,"created_by_sub":null,"created_by_name":null,"updated_by_sub":null,"updated_by_name":null,"project":{"id":5,"name":"Westside Basketball","slug":"westside-basketball","platform":"github","repo_url":"https://github.com/ldraney/west-side-basketball","is_public":true,"page_note":null,"created_at":"2026-02-26T02:00:39","updated_at":"2026-03-15T18:39:02.426531"},"tags":[{"id":5,"name":"sop"},{"id":8,"name":"active"}],"created_at":"2026-04-03T19:00:28.192523","updated_at":"2026-04-15T18:15:04.648391"}