• contact@verticalserve.com
Home / Engineering / Post 98
Engineering Blog · Post #98

When the Survey Returns 'Unknown Risk': How InsightUW Turns a Loss Control Finding into a Drafted Rescission Endorsement Without Auto-Sending Anything

From "The post-bind loss control survey came back, the surveyor flagged undisclosed asbestos in Building 3, the policy was bound two weeks ago, and the UW now has to figure out — manually — whether to rescind, endorse-out, non-renew, or warrant-and-monitor — and whether a rescission notice can even be drafted in fewer than four meetings" to "Mark survey actioned → six rules evaluate against the structured finding → a rescind recommendation auto-creates with rationale 'Loss control survey returned unknown_risk on P-2026-A8F4C2; coverage was bound based on incomplete disclosure; consider rescission' → UW accepts → a draft Policy Manuscript Amendment of type cancellation scaffolds itself for the UW to author from a starting point" — through one append-only recommendation table, six seeded rules, and the deliberate decision that accept drafts an action but never executes one.


The Problem

A binder went out, the policy was issued, three weeks pass. The post-bind loss control surveyor visits the site and finds something the application didn't disclose: asbestos in a 1968 manufacturing facility nobody mentioned. The surveyor's report uses words like "unknown_risk" and "unacceptable_risk."

Now the UW has a decision to make. Options on the table:
- Rescind the policy (treat it as if it never bound, return premium minus expense fee)
- Endorse-out the affected building or peril (asbestos exclusion endorsement, mid-term)
- Non-renew at expiration (don't rescind, but don't renew)
- Add a warrant (require remediation by a date certain or coverage suspends)
- Increase premium mid-term (consideration for the additional risk)
- Accept as-is (the underwriter has reasoned that the risk is acceptable despite the finding)

Plus the regulator audit trail has to show:
- That a recommendation was generated for this exact finding
- What the UW decided
- Why they decided it (especially if they declined to rescind)
- Who else reviewed (manager?)
- What action — if any — was actually taken

The usual fixes don't fix:

  • Free-text "next steps" field on a survey row. Two UWs facing identical findings on adjacent properties write different recommendations. Regulator: "Why?" UW: "I dunno, I just typed it." This is exactly the hole regulators want filled.
  • Hardcode the recommendation in the survey-actioned route. Every new finding type → code change. Cap-by-cap drift between LOBs.
  • Auto-execute the recommendation. Rescission is a regulator-grade decision. Auto-executing what an algorithm decided is worse than not having the algorithm.
  • Recommendation lives in the survey row. When a UW supersedes (originally rescind, manager pushed for endorse-exclusion), there's no chain — the audit trail looks like the original recommendation was wrong, not that a different one replaced it.

The root cause: the finding-to-action mapping needs to be (a) rule-driven and configurable, (b) append-only so chains are auditable, (c) scaffolds-an-action-but-doesn't-execute so humans stay in the loop, (d) cross-source because both subjectivities-not-met and surveys can fire recommendations.

The InsightUW Approach

A small recommendation table, six seeded rules, an engine that fires on two event types, and an accept path that drafts a Policy Manuscript Amendment for the UW to author from.

graph TD subgraph Triggers["Two trigger events"] Subj Nm["Subjectivity → not met<br/>(cap #11 transition)"] Survey Ac["Loss control survey<br/>mark actioned<br/>+ overall finding ∈<br/>{unknown risk, unacceptable risk,<br/>major issues}"] end subgraph Engine["Recommendation engine"] Build["Build event payload<br/>{on, category, overall finding, ...}"] Walk["Walk active Subjectivity Resolution Rule<br/>ordered by sort order"] Match["Match trigger condition json<br/>against event payload"] Emit["Emit Subjectivity Recommendation<br/>recommendation status=open<br/>severity = rule.recommended severity<br/>rationale = render(rule.rationale template)"] Notify["Notification subjectivity recommendation created<br/>severity=warning if high<br/>recipient = source.assigned to"] end subgraph Rules["6 seeded rules"] R1["survey unknown risk<br/>→ rescind / high"] R2["survey unacceptable risk<br/>→ endorse exclusion / high"] R3["survey major issues<br/>→ add warrant / medium"] R4["subjectivity not met inspection<br/>→ endorse exclusion / medium"] R5["subjectivity not met financials<br/>→ non renew / high"] R6["subjectivity not met default<br/>→ request more info / low"] end subgraph Decide["UW dashboard /uw/bind/recommendations"] Accept["Accept<br/>+ optional notes"] Reject["Reject<br/>+ Mandatory rationale<br/>(regulator audit)"] Supersede["Supersede<br/>+ replacement type<br/>+ replacement severity<br/>+ replacement rationale"] end subgraph Draft Action["Accept side effect"] Checktype{"recommendation type ∈<br/>{rescind, endorse exclusion}?"} Draft["Auto-create Policy Manuscript Amendment<br/>amendment status='draft'<br/>amendment type=cancellation OR endorsement exclusion<br/>title='Rescission per recommendation &lt;guid&gt;'<br/>body markdown carries source rationale"] Link["Subjectivity Recommendation.draft action guid<br/>= amendment guid"] Skip["No draft (UW authors from scratch)"] end Subj Nm --> Build Survey Ac --> Build Build --> Walk R1 -.-> Walk R2 -.-> Walk R3 -.-> Walk R4 -.-> Walk R5 -.-> Walk R6 -.-> Walk Walk --> Match Match --> Emit Emit --> Notify Emit --> Accept Emit --> Reject Emit --> Supersede Accept --> Checktype Checktype -->|yes| Draft Checktype -->|no| Skip Draft --> Link

Rule shape: trigger condition json matches an event payload

Each Subjectivity Resolution Rule carries:
- rule code — unique
- name — human-readable
- trigger condition json — keys: on (subjectivity_not_met / survey), category, overall finding in, priority lte
- recommended type — one of seven action types
- recommended severity — high / medium / low
- rationale template — Python str.format() template against event payload

Engine matches by checking each present key in the condition; missing keys are "don't care." All present clauses must match (logical AND).

Default seeded rules (POST /admin/rules/seed is idempotent):

Admin can edit, disable, reorder, add. The engine reads them in sort order ascending and emits one recommendation per matching rule.

Two trigger paths into one engine

fire recommendations is called from two places:

  1. Cap #11 transition when new_status == 'not_met' — builds event = {on: 'subjectivity_not_met', category: subjectivity.subjectivity_category, priority: subjectivity.priority}.

  2. Cap #11 mark survey actioned — builds event = {on: 'survey', overall_finding: survey.overall_finding}. Plus, if survey.related_subjectivity_guid is set AND the finding maps to a transition (clean→cleared, minor/major→received, unknown_risk/unacceptable_risk→not_met), the related subjectivity is auto-transitioned via the same transition machinery — which can chain into another fire recommendations call if the cascade reached not met. The two-call flow stays correct: idempotency at the rule level is by rule code per (subjectivity OR survey), so re-firing the engine on the same source doesn't duplicate.

Manual triggers exist too (generate for subjectivity / generate for survey) — useful when a UW wants to re-run the engine after editing a rule or to force-generate where automated triggers haven't fired.

The three actions: accept / reject / supersede

Subjectivity Recommendation.recommendation_status lifecycle:

Accept:
- Optional resolution notes from the UW
- For recommendation_type ∈ {rescind, endorse_exclusion}: scaffolds a draft Policy Manuscript Amendment (Quote cap #8 entity) with amendment_status='draft', type cancellation or endorsement exclusion, body carrying the recommendation guid + rationale. The draft action guid links recommendation to amendment.
- For other types (non renew, add warrant, increase premium, accept as is, request more info): no auto-draft. UW authors from scratch via the existing amendment flow. The draft action guid stays null.

The draft is not executed. It's the starting point for the UW's authoring. Final cancellation notices, exclusion endorsements, premium increases — all of those go through their existing review + finalize + PAS push paths. Cap #11's accept just bridges "engine recommended X" to "amendment draft of type X exists."

Reject requires a non-empty rationale string. Empty → 400. Why so strict? Regulators want a documented "we considered the recommendation and chose not to act." Free-text empty doesn't cut it; "n/a" cuts it because somebody typed it.

Supersede creates a fresh Subjectivity Recommendation row with the replacement attributes (type, severity, rationale) AND links the old row's superseded by recommendation guid to the new guid. Append-only. The UW dashboard shows the chain visually: original → superseded → current.

Append-only with chains

Every action writes a Audit Entry. The recommendation table itself is mutated only in:
- recommendation status (open → accepted / rejected / superseded)
- resolution_* fields (notes, resolved_at, resolved_by)
- draft action guid (set on accept for the two amenable types)
- superseded by recommendation guid (set on supersede)

The recommendation type, severity, rationale, trigger rule code, policy guid, source subjectivity guid / source survey guid — frozen at create. The audit trail says: "Engine recommended X at time T because rule R fired against event E. UW chose Y at time T+N because reason Z."

Worked Example: Acme Manufacturing $25M Property, Asbestos Surprise

P-2026-A8F4C2 was bound 2026-04-26. The application listed Buildings 1, 2, and 4 — Building 3 was described as "storage, no operations." Post-bind loss control survey scheduled for 2026-05-12.

Step 1 — Surveyor visits, finds undisclosed asbestos

The surveyor's site visit on 2026-05-12 reveals asbestos-containing materials in Building 3's roof and pipe insulation, with active deterioration. The application called Building 3 "storage" but Acme had been intermittently occupying it for low-volume R&D operations. Surveyor's structured findings:

UW Sarah opens the loss-control-survey-modal on the bind-request-detail. She fills in the surveyor's name, copies the finding categories, attaches the PDF report (Submission Document with document_category='loss_control_survey'). Saves; survey is in pending review state.

Step 2 — Mark actioned

Sarah clicks Mark actioned (run engine). Service:

  1. Sets survey.uw_review_status='actioned', uw_reviewed_at=now, uw_reviewed_by=sarah.
  2. Builds event = {"on": "survey", "overall_finding": "unknown_risk"}.
  3. Walks rules. Match: survey unknown risk (sort_order 10).
  4. Renders rationale: "Loss control survey returned 'unknown_risk' on policy P-2026-A8F4C2. Coverage was bound based on incomplete disclosure; consider rescission."
  5. Inserts Subjectivity Recommendation:
  6. Notifies Sarah: subjectivity recommendation created, severity=warning, "RESCIND recommended (high)".
  7. (No related subjectivity guid was set on this survey because it wasn't tied to a specific subjectivity — so no auto-transition fires.)

Step 3 — Sarah opens the recommendations dashboard

/uw/bind/recommendations filtered to open. The new row:

Sarah doesn't decide alone — rescission has compliance, claims, and legal implications. She schedules a 30-min call with her senior UW manager Marcus, the head of Property compliance, and Legal.

The call lands on a different decision: endorse-out Building 3 with an asbestos-specific exclusion plus a warrant requiring a certified ACM assessment within 60 days. Rescinding a 2-week-old policy in a soft market would be a relationship-killer; the broker had no reason to think they were misrepresenting; the carrier can manage the risk via endorsement.

Step 4 — Sarah supersedes with replacement

She clicks Supersede on the rescind recommendation:

Service:
1. Original rescind row → recommendation_status='superseded', superseded_by_recommendation_guid=<new>.
2. New Subjectivity Recommendation row inserted:
3. Audit entries on both rows.

UI now shows two rows on the policy: the superseded rescind (gray, with link to replacement) and the open endorse_exclusion.

Step 5 — Sarah accepts the endorse_exclusion

Click Accept. Modal shows: "Accepting will scaffold a draft amendment in the Quote module." Sarah types resolution_notes='Per Property compliance + Legal — proceeding with asbestos exclusion endorsement + ACM warrant per superseded recommendation.'. Confirm.

Service:
1. recommendation row → recommendation_status='accepted', resolution notes, resolved_at=now, resolved_by=sarah.
2. Auto-creates Policy Manuscript Amendment:
3. recommendation.draft_action_guid = <amendguid>.
4. Audit + notification.

UI on the recommendation row now shows "→ Draft action: <amendguid[:8]>" linking to the Quote-side amendment authoring page where Sarah completes the asbestos exclusion wording and the ACM warrant clause. Cap #11 is done; Quote cap #8's existing amendment + PAS-push flow takes over.

Step 6 — Audit forever

Six months later a regulator audits the file. They see:

Every byte of the decision is on rows. There's no email to retrieve, no Slack thread to subpoena, no "what did the team decide on May 13" guesswork. The chain is durable.

What This Means for Underwriters

  1. Findings drive recommendations, recommendations don't execute themselves. The engine emits an open recommendation; the UW always decides; accept on rescind/endorse drafts an amendment — never sends one.

  2. Mandatory rationale on reject. Regulators want "we considered rescinding and chose not to" documented. The system enforces a non-empty string.

  3. Supersession chains are visible. When the team's decision evolves from rescind → endorse_exclusion, the original isn't deleted — it's superseded with a link to the replacement. The decision evolution is itself the audit.

  4. Survey-actioned and subjectivity-not-met share one engine. Two trigger paths, six rules, one recommendation table. New finding type → new rule, not a new code path.

  5. Configurable rules, no redeploy. Admin edits trigger condition json, severity, rationale_template, sort_order through the rule admin UI. New rule active immediately.

  6. Two recommendation types auto-draft. Rescind and endorse_exclusion scaffold a Policy Manuscript Amendment because they're the two with concrete document outputs. The other five (non-renew, add_warrant, increase_premium, accept_as_is, request_more_info) require the UW to author from scratch — by design; their actions vary too much for a useful draft.

  7. The recommendation lives on the policy, not the survey. Surveys come and go (annual loss control); the policy persists. A second survey eight months later that fires another recommendation joins the same per-policy thread.

  8. All-clear on subjectivities is independent of recommendations. Subjectivities reaching cleared/waived fires subjectivity all clear once per quote (tracked via subjectivities_summary_json.all_clear_notified). Recommendations are post-clearance; they live on the policy regardless.

What's Next

Next: Templates and PDFs and Outboxes: How InsightUW Renders a Binder Twice (PDF + DOCX) and Sends It Through the Existing Email Plumbing Without Forking Anything


Want to see how a six-rule engine, a draft-not-execute accept path, and an auditable supersession chain close the "what did we decide and why" gap on post-bind loss control findings? Request a demo.

See InsightUW run on your data

A 45-minute working session with a real broker email and your LOBs.

Request a demo