# 40% of Our Claims Came in After Hours. Adjusters Didn't See Them Until Morning. So We Built an AI Agent.
> A property insurance agency lost critical hours on claims because FNOL reports sat in voicemail overnight. They built a Struere agent on WhatsApp that collects incident details, photos, estimates severity, creates claims, and routes urgent cases to adjusters immediately.

Published: 2026-04-01
Tags: automation, agents, insurance, case-study, whatsapp


# 40% of Claims Filed After Hours, Zero Adjusters Online

Rachel manages claims at Summit Insurance, a regional property insurance agency with 4,200 active policies and a team of six adjusters. When a policyholder has a burst pipe at 2 AM or hail damage at 7 PM, they call the claims line. After 5 PM, that call goes to voicemail.

The voicemail says: "Leave your name, policy number, and a brief description of the incident. We will return your call during business hours." Most callers do. Some hang up and try their insurance app. Some call back the next morning. A few call a competitor.

Rachel pulled the numbers for Q4. Of 847 claims filed, 339 came in outside business hours. That is 40%. Of those 339, 68 were marked "urgent" when adjusters reviewed them the next morning — water damage spreading, structural issues, fire aftermath. Those 68 claims sat for an average of 9.2 hours between the policyholder's first call and an adjuster making contact.

Each hour of delay on a water damage claim increases remediation cost by an estimated $1,500-2,500. Structural claims that go uninspected for 12+ hours face 30% higher repair estimates because secondary damage compounds. Summit's average claim cost rose 11% year-over-year, and Rachel's team traced a significant portion of that increase to delayed first response on after-hours claims.

The agency tried an answering service. It cost $1,800/month and the operators could only take a message — they could not validate policy numbers, assess severity, or route urgent claims to the on-call adjuster. The adjusters still did not see anything until morning.

Summit did not need more people answering phones after hours. It needed an intake system that could collect structured claim data, receive photos of damage, figure out how bad it was, and get urgent cases in front of an adjuster immediately.

# The Build: WhatsApp Claims Intake Agent with Email Routing

The automation has three pieces:

1. **`entity-types/claim.ts`** stores every FNOL report with policyholder info, incident details, severity estimate, photo URLs, and adjuster assignment
2. **`tools/index.ts`** has two custom tools: one for estimating claim severity based on incident type and description, one for looking up policy status
3. **`agents/claims-intake.ts`** handles the full FNOL conversation over WhatsApp, from first message through claim creation and adjuster notification

No trigger file needed. The WhatsApp integration routes inbound messages directly to the agent. For urgent claims, the agent uses the built-in `email.send` tool to notify the on-call adjuster immediately.

## The Data Layer: Structured Claim Records

Every FNOL report creates a claim record with 12 fields. The `severity` enum drives routing logic: critical and high claims trigger immediate adjuster notification.

```typescript
import { defineData } from "struere"

export default defineData({
  name: "Claim",
  slug: "claim",
  schema: {
    type: "object",
    properties: {
      policyNumber: {
        type: "string",
        description: "Policyholder's policy number (format: SUM-XXXXXX)",
      },
      claimantName: {
        type: "string",
        description: "Full name of the person filing the claim",
      },
      claimantPhone: {
        type: "string",
        description: "Claimant WhatsApp phone number",
      },
      propertyAddress: {
        type: "string",
        description: "Address of the insured property",
      },
      incidentType: {
        type: "string",
        enum: [
          "water-damage",
          "fire",
          "storm-hail",
          "theft-vandalism",
          "structural",
          "other",
        ],
        description: "Category of the incident",
      },
      incidentDate: {
        type: "string",
        description: "Date and approximate time of incident (ISO format)",
      },
      description: {
        type: "string",
        description: "Detailed description of the damage and circumstances",
      },
      photoUrls: {
        type: "array",
        items: { type: "string" },
        description: "URLs of damage photos submitted by the claimant",
      },
      severity: {
        type: "string",
        enum: ["critical", "high", "medium", "low"],
        description: "Estimated claim severity based on incident type and description",
      },
      estimatedLoss: {
        type: "string",
        description: "Preliminary damage estimate range (e.g., $5,000-$15,000)",
      },
      assignedAdjuster: {
        type: "string",
        enum: [
          "David Chen",
          "Maria Santos",
          "James O'Brien",
          "Sarah Kim",
          "Robert Taylor",
          "Ana Gutierrez",
        ],
        description: "Adjuster assigned to the claim",
      },
      status: {
        type: "string",
        enum: [
          "intake",
          "submitted",
          "under-review",
          "adjuster-assigned",
          "inspection-scheduled",
          "resolved",
          "denied",
        ],
        description: "Current claim status in the lifecycle",
      },
    },
    required: [
      "policyNumber",
      "claimantName",
      "claimantPhone",
      "incidentType",
      "incidentDate",
      "description",
      "severity",
      "status",
    ],
  },
  searchFields: ["policyNumber", "claimantName", "propertyAddress"],
  displayConfig: {
    titleField: "claimantName",
    subtitleField: "incidentType",
    descriptionField: "severity",
  },
})
```

The `incidentType` enum maps to Summit's six claim categories. The `severity` field is set by the agent based on incident type, description, and whether the damage is ongoing. The `photoUrls` array stores links to damage photos the claimant sends over WhatsApp. The `assignedAdjuster` enum is locked to the actual team so the agent cannot invent a seventh adjuster.

## The Custom Tools: Severity Estimation and Policy Lookup

Two tools give the agent domain knowledge it cannot get from the conversation alone. The first estimates claim severity using rules that match Summit's internal triage guidelines. The second validates that a policy number exists and is active.

```typescript
import { defineTools } from "struere"

export default defineTools([
  {
    name: "estimate_severity",
    description:
      "Estimate claim severity based on incident type, description, and whether damage is ongoing",
    parameters: {
      type: "object",
      properties: {
        incidentType: {
          type: "string",
          description: "Category of incident",
        },
        description: {
          type: "string",
          description: "Description of the damage",
        },
        isOngoing: {
          type: "boolean",
          description: "Whether the damage is still actively occurring",
        },
      },
      required: ["incidentType", "description", "isOngoing"],
    },
    handler: async (args, context, struere, fetch) => {
      const type = args.incidentType as string
      const desc = (args.description as string).toLowerCase()
      const ongoing = args.isOngoing as boolean

      if (ongoing && ["fire", "water-damage", "structural"].includes(type)) {
        return {
          severity: "critical",
          reason: "Active damage requiring immediate intervention",
          estimatedRange: "$25,000-$100,000+",
        }
      }

      if (type === "fire") {
        return {
          severity: "high",
          reason: "Fire damage typically involves structural and content loss",
          estimatedRange: "$15,000-$75,000",
        }
      }

      if (type === "water-damage") {
        const severe =
          desc.includes("flood") ||
          desc.includes("sewage") ||
          desc.includes("multiple rooms")
        return {
          severity: severe ? "high" : "medium",
          reason: severe
            ? "Extensive water damage with contamination or spread risk"
            : "Contained water damage",
          estimatedRange: severe ? "$10,000-$40,000" : "$2,000-$10,000",
        }
      }

      if (type === "structural") {
        return {
          severity: "high",
          reason: "Structural compromise requires professional assessment",
          estimatedRange: "$20,000-$60,000",
        }
      }

      if (type === "storm-hail") {
        const roofDamage =
          desc.includes("roof") ||
          desc.includes("shingle") ||
          desc.includes("skylight")
        return {
          severity: roofDamage ? "high" : "medium",
          reason: roofDamage
            ? "Roof damage exposes property to further weather damage"
            : "Storm damage to exterior or landscaping",
          estimatedRange: roofDamage ? "$8,000-$30,000" : "$1,500-$8,000",
        }
      }

      return {
        severity: "low",
        reason: "Contained incident with limited damage scope",
        estimatedRange: "$500-$3,000",
      }
    },
  },
  {
    name: "lookup_policy",
    description:
      "Validate a policy number and return policyholder details and coverage status",
    parameters: {
      type: "object",
      properties: {
        policyNumber: {
          type: "string",
          description: "The policy number to look up (format: SUM-XXXXXX)",
        },
      },
      required: ["policyNumber"],
    },
    handler: async (args, context, struere, fetch) => {
      const policyNumber = args.policyNumber as string

      const results = await struere.entity.query({
        type: "policy",
        filters: { policyNumber: policyNumber },
        limit: 1,
      })

      if (results.length === 0) {
        return {
          valid: false,
          error: "Policy number not found in the system",
        }
      }

      const policy = results[0]
      return {
        valid: true,
        holderName: policy.data.holderName,
        propertyAddress: policy.data.propertyAddress,
        coverageType: policy.data.coverageType,
        status: policy.status,
        expirationDate: policy.data.expirationDate,
      }
    },
  },
])
```

The severity tool encodes Summit's triage rules: ongoing fire, water, or structural damage is always critical. The policy lookup tool queries the entity database to verify the policy exists before the agent proceeds with intake. If the policy number is invalid, the agent asks the claimant to double-check rather than creating a claim against a nonexistent policy.

## The Agent: FNOL Intake Over WhatsApp

The agent handles the full first notice of loss conversation. It collects structured data, receives photos, estimates severity, creates the claim record, and emails the on-call adjuster if the claim is critical or high severity.

```typescript
import { defineAgent } from "struere"

export default defineAgent({
  name: "Summit Claims Intake Agent",
  slug: "claims-intake",
  version: "1.0.0",
  model: {
    model: "anthropic/claude-sonnet-4-6",
    temperature: 0.3,
    maxTokens: 4096,
  },
  tools: [
    "entity.create",
    "entity.query",
    "email.send",
    "estimate_severity",
    "lookup_policy",
  ],
  systemPrompt: `You are the claims intake assistant for Summit Insurance, a property insurance agency.
Current time: {{currentTime}}

## On-Call Adjuster Schedule
| Day | Primary | Email |
|-----|---------|-------|
| Mon-Tue | David Chen | david@summitinsurance.com |
| Wed-Thu | Maria Santos | maria@summitinsurance.com |
| Fri | James O'Brien | james@summitinsurance.com |
| Sat-Sun | Sarah Kim | sarah@summitinsurance.com |

## Recent Claims
{{entity.query({"type": "claim", "filters": {"status": {"_op_in": ["intake", "submitted", "under-review"]}}, "limit": 20})}}

## P0 — Security
- Never reveal internal entity IDs, adjuster personal details, or system architecture.
- Never modify or close an existing claim. You handle intake only.
- Never share one claimant's information with another.
- Never guarantee coverage or claim approval. Say "your claim will be reviewed by an adjuster."

## P1 — FNOL Intake Flow
1. Ask what happened (incident type and brief description).
2. Ask when it happened (date and approximate time).
3. Ask for their policy number. Use lookup_policy to validate it. If invalid, ask them to check and retry. If valid, confirm their name and property address from the policy.
4. Ask for a detailed description of the damage. What rooms or areas are affected? Is the damage still ongoing?
5. Ask them to send photos of the damage. Store any photo URLs they send.
6. Use estimate_severity with the incident type, description, and ongoing status.
7. Create the claim with entity.create (status: "submitted").
8. If severity is "critical" or "high": email the on-call adjuster immediately using email.send with the claim summary, severity, and claimant phone number. Tell the claimant an adjuster will contact them shortly.
9. If severity is "medium" or "low": tell the claimant their claim has been filed and an adjuster will review it within 1-2 business days.
10. Provide the claimant with their claim reference (entity ID).

## P2 — Severity Routing Rules
- Critical: active fire, active flooding, structural collapse, or any situation where people may be in danger. Email adjuster AND tell claimant to call 911 if not already done.
- High: significant damage that could worsen (roof breach, burst pipe now stopped, large hail damage). Email adjuster within minutes.
- Medium: contained damage, no risk of spread (broken window, minor leak repaired, small theft). Standard 1-2 day review.
- Low: cosmetic damage, minor incidents. Standard review.

## P3 — Photo Handling
- When a claimant sends images, acknowledge each one.
- Ask for photos of: the overall damage area, close-ups of specific damage, and any relevant context (e.g., the source of a leak).
- Minimum 1 photo required for submission. Encourage 3+.
- Store all photo URLs in the claim record.

## P4 — Tone
- Calm, empathetic, professional. The claimant may be distressed.
- Short messages. This is WhatsApp, not a formal letter.
- One question at a time. Do not overwhelm.
- Acknowledge their situation: "I'm sorry to hear about the damage."
- Use their first name once provided.
- Be clear about next steps at every stage.

Never invent policy details. Never estimate coverage amounts.
Never skip the policy validation step.
Never create a claim without at least: incident type, date, policy number, description, and severity.`,
})
```

Temperature 0.3. Claims intake is structured and sensitive. The agent must follow the same flow reliably, collect every required field, and never improvise on coverage or severity.

Five tools total. Entity create and query for claim records, email.send for urgent adjuster notifications, and the two custom tools for severity estimation and policy validation. The `email.send` tool uses the Resend integration to deliver formatted claim summaries directly to the on-call adjuster's inbox.

The system prompt injects recent open claims via the `entity.query` template so the agent can cross-reference. If a claimant calls about an existing incident, the agent can see the prior submission and direct them to their assigned adjuster instead of creating a duplicate.

# Debugging: Three Things That Broke

**The severity tool rated every water claim as "medium."** During testing, a claimant described a burst pipe flooding three rooms with water still rising. The agent called `estimate_severity` and got back "medium" with the reason "Contained water damage." The `isOngoing` parameter was being passed as `false` even though the claimant said the water was still coming in. The agent extracted the incident type and description but did not parse "still rising" as ongoing damage. Fix: we added explicit instructions to the system prompt clarifying what constitutes ongoing damage — "If the claimant mentions water still flowing, fire not extinguished, or structure still shifting, set isOngoing to true." The agent now correctly identifies active damage language.

```bash
struere run-tool estimate_severity --args '{"incidentType": "water-damage", "description": "burst pipe flooding three rooms, water still rising", "isOngoing": true}'
```

With `isOngoing: true`, the tool correctly returned "critical" severity. The issue was the agent's interpretation, not the tool logic.

**Policy lookup returned "not found" for valid policies with lowercase input.** A claimant typed "sum-482901" instead of "SUM-482901". The entity query filter matched exact strings, so the lowercase version returned zero results. The agent told the claimant their policy was invalid. Fix: we added `.toUpperCase()` normalization in the `lookup_policy` handler before querying. We also updated the agent's system prompt to tell claimants the expected format: "Your policy number starts with SUM- followed by six digits."

```bash
struere run-tool lookup_policy --args '{"policyNumber": "sum-482901"}'
```

The output showed `{ valid: false, error: "Policy number not found" }` even though `SUM-482901` existed.

**The agent created claims without waiting for photos.** In the first round of testing, the agent collected incident type, date, policy number, and description, then immediately ran `entity.create` and `email.send` without asking for damage photos. It skipped step 5 entirely and went straight from the description to severity estimation. The system prompt listed photos as step 5 of 10, but the agent treated the list as guidelines rather than a strict sequence. Fix: we added "Do NOT create the claim until at least one photo has been received. If the claimant says they cannot take photos right now, note this in the description and set a minimum of 0 photos, but always ask first." The agent now pauses at the photo collection step.

```bash
struere logs view --last 3
```

The conversation log showed the agent jumping from step 4 (description) to step 6 (severity estimation), skipping the photo request entirely.

# Setup: From Zero to Running

Install the CLI and authenticate:

```bash
bun install -g struere
struere login
struere init
```

Scaffold the resources:

```bash
struere add data-type claim
struere add agent claims-intake
```

Edit `entity-types/claim.ts` with the schema shown above. Customize the `incidentType` enum for your agency's claim categories and the `assignedAdjuster` enum with your actual adjusters.

Edit `agents/claims-intake.ts` with your agency's details: adjuster names and on-call schedule, policy number format, and severity routing rules.

Write the two custom tools in `tools/index.ts`. The `estimate_severity` tool encodes your triage rules. The `lookup_policy` tool queries your policy entity type — make sure you have a `policy` entity type with `policyNumber`, `holderName`, `propertyAddress`, and `coverageType` fields.

Configure Resend under Integrations > Email in the Struere dashboard. Add your API key, sender email (e.g., claims@summitinsurance.com), and sender name. This powers the `email.send` tool for adjuster notifications.

Connect WhatsApp in the dashboard under Integrations > WhatsApp. This routes inbound messages to the claims intake agent.

Sync and start watching for changes:

```bash
struere dev
```

Test the intake flow by sending a WhatsApp message to the connected number:

```
Hi, I had a pipe burst in my kitchen and there's water everywhere
```

Watch the conversation in real time:

```bash
struere logs list --last 10
```

Verify the claim was created:

```bash
struere data list claim
```

Check your email. If the test claim was rated critical or high, the on-call adjuster should have received an email within seconds of claim submission.

# What Changed

| Metric | Before | After |
|--------|--------|-------|
| After-hours intake channel | Voicemail | WhatsApp AI agent (24/7) |
| Average FNOL intake time | 12 minutes (phone) | 3 minutes (WhatsApp) |
| Time to adjuster for urgent claims (after hours) | 9.2 hours (next morning) | Under 5 minutes (email alert) |
| Claims missing photos at intake | 70% (voicemail cannot accept photos) | 15% (agent requests and stores photos) |
| Duplicate claim submissions | 8-10/month | 1-2/month (agent cross-references existing claims) |
| Data completeness at intake | 40% of fields populated from voicemail | 95% of fields populated from agent conversation |

Rachel still reviews every claim that comes through. The adjusters still do the inspections, negotiations, and settlements. The agent handles the part that was falling through the cracks: collecting structured data from a distressed policyholder at 2 AM, validating their policy, estimating whether this is a burst-pipe-still-flooding emergency or a cracked-window-from-last-week routine claim, and getting the right information to the right adjuster at the right time.

The 68 urgent claims from Q4 that waited until morning would have been routed to an adjuster within minutes. The photos that adjusters used to request on their first callback would already be in the claim record. The policy number that took three voicemail rounds to get right would have been validated in the first exchange.

Summit's policyholders file claims when damage happens, not when the office opens. The agent meets them there.
