Two Renders, One Outbox, One Sweeper: How InsightUW Generates the Binder PDF + DOCX, Sends Through the Existing Email Plumbing, and Reconciles Delivery Without a Webhook Receiver
From "we hand-build the binder in Word, the AC emails it from a personal Outlook account, the broker confirms receipt over the phone, three weeks later the audit asks 'what did we send and when?' and the answer is in someone's Sent folder" to "Generate runs the existing template engine twice — PDF for the broker, DOCX for internal review — both stored as Submission Document rows; Send wires through the existing Email Send Service outbox; a 5-min sweeper reads the outbox status and advances the binder lifecycle from sent → delivered without a webhook receiver" — through one new entity, three render reuses, and the deliberate refusal to build a parallel email pipeline for "binder transmission."
The Problem
A binder is the temporary contract that confirms coverage is in force ahead of the formal policy document. Every binder has the same structure: insured info, coverage summary, effective dates, premium, policy number, signature block. Every binder has to be:
- Rendered from a template (different LOBs and clients have different formats)
- Available in two formats — PDF for clean broker delivery, DOCX for internal review and edits
- Versioned — amendments mid-term mean a new binder; the prior is superseded but visible in audit
- Sent via a real email pipeline with attachments, idempotent on retry, with delivery / failure reconciled
- Deliverable to multiple parties (broker is To; UW + AC + sometimes broker manager are CC)
The usual fixes don't fix:
- A new "binder service" with its own template engine. Quote already has generate from template that handles HTML+Jinja → PDF, DOCX upload → DOCX render, brand colors, header/footer, snapshot audit. Building a parallel engine forks the operational story.
- A new "binder email service" with its own outbox. Quote cap #4's Email Send Service already does composition, attachment, account routing, sender rewriting, retry with backoff, dead-letter. Cap #6 needs none of those re-implemented; it needs to call them.
- Webhook receivers from the email provider. Reading delivery status via webhook means standing up an HTTPS endpoint, validating signatures, handling provider-specific retry semantics. The existing outbox already tracks send status per row; cap #6 just needs a sweeper that reads it.
- Hardcoded binder template. Different clients want different layouts. Per-LOB defaults exist; admin should be able to add per-client templates without a deploy.
The root cause: the binder workflow is a thin layer on top of three already-built systems. Cap #6 has to be the layer, not the systems.
The InsightUW Approach
One new entity for the binder lifecycle; reuse the Quote template engine for rendering; reuse the Quote email outbox for sending; one sweeper for delivery reconciliation.
Two renders from one template
generate from template (Quote cap #1) takes output format as an argument. Calling it twice — once with 'pdf', once with 'docx' — produces both formats from the same template + merge context. Each render writes a Submission Document row with document_category='binder', the same template_guid + version, and the rendered bytes in storage.
The binder row stores both:
The PDF goes to the broker as the email attachment. The DOCX is downloadable from the binder detail UI for internal review and hand-edits — UWs sometimes want to copy clauses into a Word document for ad-hoc work, and DOCX preserves the editable structure.
If DOCX render fails (template authored only for HTML→PDF), the PDF still goes through. DOCX is best-effort.
Versioning via supersession
Binder.version increments. Active binder = binder_status NOT IN ('superseded'). When generate runs and an active binder exists:
The history disclosure on the binder panel shows superseded versions; the active one is the lone non-superseded row. Bind Request's pointer column stays in sync with the active PDF — UI can read it without joining Binder.
Send through the existing outbox
Email Send Service.send() is the public API from Quote cap #4. Cap #6 calls it with:
The send-service handles:
- Account resolution (which sender?)
- Insert of Email Platform Message (canonical email row)
- Insert of Email Attachment rows
- Insert of Email Outbox (worker picks it up within 30s)
- Auto-categorize for the email-trail filter
Cap #6's job is to pass the right inputs and stamp the outbox guid back on the binder row:
The sweeper that replaces the webhook receiver
A 5-minute sweeper task reads Email Outbox.send_status for every binder.binder_status='sent' row that has an email outbox guid:
No webhook receiver. No HTTPS endpoint to provision. No signature validation. The email send pipeline already tracks delivery status; cap #6 just polls it on its own cadence.
For binders that just transitioned to delivered, the sweeper also fires the cap #7 hook auto push on binder delivered — if the LOB's Issuance Config.issuance_trigger='on_binder_delivered', the policy_issued PAS push queues. (If the trigger is manual or on bound, the hook short-circuits.)
Retry-send creates a fresh outbox row
Failed send. UW clicks Retry on the binder panel. Service:
- Verifies
binder_status == 'failed' - Rolls binder_status back to 'reviewed' (so the send guard accepts it — send requires status ∈ {generated, reviewed})
- Calls send with the original or updated to/cc/text — which inserts a new Email Outbox row
- Updates
binder.email_outbox_guidto the new guid
The prior outbox row stays untouched for audit. If a regulator asks "what did the second attempt look like vs. the first," the answer is two rows in the outbox table linked to two send attempts on the same binder version.
Per-LOB auto-config
Binder Auto Config carries per-LOB toggles:
- auto generate on bound — fire generate automatically from cap #3's bind hook (default off; clients flip on after they've reviewed the rendered output)
- default template guid — which template to use for this LOB
- require review before send — send refuses if binder is still in generated (not yet reviewed)
Default starter binder template seeds on startup (idempotent on document_type='binder' AND template_status='active'). Clients customize from there.
Worked Example: Acme Energy $50M Excess Casualty, Two Versions of the Binder
P-2026-EX-7104 was bound 2026-04-26. UW Sarah needs a binder out by EOD; broker is presenting to insured tomorrow morning.
Step 1 — Generate
She opens bind-request-detail. Binder panel shows "none" with a "Generate binder" button. Click.
Service:
1. Verifies bind request status is bound ✓
2. Verifies no active binder exists ✓
3. Resolves template: no per-LOB config for excess casualty, falls through to "any active binder template" → finds the seeded "Default binder" (active, jurisdiction=BOTH, source_format=html_jinja).
4. Builds merge context (Bind Request + Quote + Policy + Insured + Broker + Binder self-reference for binder_number).
5. Calls generate from template → Submission Document PDF row (file_name="binder_QT-2026-EX-3819.pdf").
6. Calls again with output_format='docx' → Submission Document DOCX row.
7. Inserts Binder:
8. Updates Bind Request.binder_document_guid = PDF guid.
9. Audit + notification (UW + AC).
10. Commit.
UI refreshes; binder panel now shows status generated, version 1, "Mark reviewed" + "Send" + "Regenerate" actions.
Step 2 — Quick review
Sarah downloads the PDF (existing Submission Document download flow), eyeballs it. Manuscript wording renders correctly; layered structure shows; effective dates correct. She clicks "Mark reviewed" — modal asks for optional notes — types "Reviewed; ready to send" → confirms.
binder.binder_status = 'reviewed', reviewed at, reviewed by, review notes set.
Step 3 — Send
She clicks Send. Modal pre-fills:
- to_email: pulled from broker contact (broker.email)
- cc: defaults to UW (sarah@) + AC (mike@)
- additional_text: empty
She types in additional_text: "Hi Lisa — binder for the bound program. Insured presentation is tomorrow 10am ET; please confirm receipt before then." Submit.
Service:
1. Verifies binder_status ∈ {generated, reviewed} ✓ (it's 'reviewed')
2. Excess Casualty LOB has no Binder Auto Config row → require_review_before_send defaults to false → guard passes.
3. Builds attachments (PDF only by default; DOCX skipped for broker email — DOCX is internal).
4. Calls Email Send Service.send() with subject "Binder B-2026-A8F4C2 — please confirm receipt", body HTML, PDF attachment.
5. Send service inserts Email Platform Message + Email Attachment + Email Outbox (send_status='pending', next_try_at=now).
6. Returns {outbox_guid, msg_guid}.
7. Cap #6 stamps binder.binder_status='sent', email outbox guid, sent at, sent_to_email='lisa@brokeragecorp.com', cc_recipients_json=["sarah@", "mike@"], additional_text=<text>.
8. Audit + notify UW + AC.
9. Commit.
Email worker (separate, every 30s) picks up the outbox row, sends via the configured SMTP/Graph account, sets outbox send_status='sent' on success.
Step 4 — Sweeper reconciles delivery
5 minutes later, the binder delivery sweeper runs:
Acme's outbox row is send_status='sent' (the email worker already delivered to the broker mailbox). Binder advances to delivered. delivered_at = 2026-04-26 17:14:23 UTC.
Sweeper then fires cap #7 hook:
For excess_casualty LOB, no Issuance Config row → trigger defaults to 'manual' → hook short-circuits with {"skipped": "trigger=manual"}. No PAS issued push fires; UW will push manually when she's ready.
Step 5 — Day-3 amendment
A week later, the broker negotiates a manuscript wording change — adds a $2M sublimit on D-class claims. Sarah authors the amendment via Quote cap #8's existing flow; once finalized, she comes back to bind-request-detail and clicks Regenerate on the binder.
Service:
1. Marks current binder binder_status='superseded', superseded_at=now.
2. Generates fresh PDF + DOCX (template hasn't changed; merge context now reflects the amended terms).
3. Inserts new Binder version=2, links via superseded by binder guid.
4. Updates Bind Request.binder_document_guid to new PDF guid.
Sarah reviews v2, sends. New email outbox guid, new send. Sweeper advances to delivered. The history disclosure on the binder panel shows:
Step 6 — Audit forever
Twelve weeks later a regulator audits the bound program. They see two binder rows on the same bind request, two corresponding Submission Document PDFs in storage, two Email Platform Message rows with full body + recipient archive, two Email Attachment rows linking the right PDFs. The chain is complete.
What This Means for Underwriters
-
One template engine, two formats. PDF and DOCX produced from the same source + merge context + audit snapshot. Render twice, store twice.
-
Existing outbox does the actual sending. Cap #6 doesn't reimplement SMTP, OAuth, retries, dead-letter, or any provider-specific quirk.
Email Send Service.send()returns an outbox guid; cap #6 stamps it. -
Sweeper polls instead of webhook. No HTTPS endpoint. No signature validation. The 5-min cadence is good enough for human-pace work; production-grade webhooks can be added later if a client demands sub-minute reconciliation.
-
Versioning via supersession is irreversible by design. Regenerate creates v2; v1 stays for audit. Bind Request's pointer column always reflects the active version.
-
Retry creates a fresh outbox row. Prior failure stays for audit; a second attempt is a new outbox row linked to the same binder version. Two attempts, two rows, one audit story.
-
Auto-generate on bound is per-LOB and defaults off. Clients flip it on after reviewing the rendered output for their LOB; default config doesn't surprise anyone.
-
PDF only to the broker; DOCX downloadable internally. Cleaner email; smaller attachment; the DOCX still exists for ad-hoc internal handling.
-
Cap #7 issued-push hook fires on delivered. When LOB config says on binder delivered, the policy_issued PAS message queues automatically. UW doesn't have to remember to click Mark issued.
What's Next
Want to see how a thin binder layer reuses the template engine, the email outbox, and a 5-minute sweeper to deliver a regulator-grade audit trail without forking infrastructure? Request a demo.