AI Agent for Roofing Companies: Damage Assessment, Insurance Claims, and Scheduling
Build an AI agent for roofing companies that assists with damage assessment from photos, generates insurance claim documentation, manages insurance workflows, and schedules repair crews.
The Roofing Business Workflow
Roofing companies operate in a unique space where most revenue comes through insurance claims after storm damage. The workflow is complex: inspect the roof, document damage with photos and measurements, generate a detailed scope of work using Xactimate pricing, submit the claim to the insurance carrier, negotiate supplements, schedule the repair once approved, and manage crews across multiple active projects. An AI agent that handles documentation, claim preparation, and scheduling can cut the time from inspection to repair start by 40%.
The most valuable automation is claim documentation. Insurance adjusters reject claims with insufficient or poorly organized documentation. An AI agent ensures every claim package is thorough and formatted to the carrier's requirements.
Damage Assessment from Inspection Data
Roof inspections generate photos, measurements, and field notes. The agent structures this raw data into a formal damage assessment.
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Optional
class DamageType(Enum):
HAIL = "hail"
WIND = "wind"
FALLEN_TREE = "fallen_tree"
AGE_WEAR = "age_wear"
WATER = "water"
MISSING_SHINGLES = "missing_shingles"
class DamageSeverity(Enum):
MINOR = "minor" # Cosmetic, no leak risk
MODERATE = "moderate" # Functional damage, leak possible
SEVERE = "severe" # Active leak or structural compromise
TOTAL_LOSS = "total_loss" # Full replacement required
@dataclass
class DamageArea:
area_id: str
location: str # "north slope", "ridge", "valley"
damage_type: DamageType
severity: DamageSeverity
size_sqft: float
photo_urls: list[str] = field(default_factory=list)
notes: str = ""
@dataclass
class RoofAssessment:
property_address: str
inspection_date: datetime
roof_type: str # "asphalt_shingle", "metal", "tile", "flat"
total_sqft: float
pitch: str # "4/12", "6/12", "8/12"
stories: int
damage_areas: list[DamageArea] = field(default_factory=list)
storm_date: Optional[datetime] = None
@property
def total_damage_sqft(self) -> float:
return sum(area.size_sqft for area in self.damage_areas)
@property
def damage_percentage(self) -> float:
return (self.total_damage_sqft / self.total_sqft * 100) if self.total_sqft else 0
def recommendation(self) -> str:
if self.damage_percentage > 25 or any(
a.severity == DamageSeverity.TOTAL_LOSS for a in self.damage_areas
):
return "full_replacement"
elif self.damage_percentage > 10:
return "partial_replacement"
else:
return "repair"
Insurance Claim Documentation Generator
Insurance claims require specific documentation formats. The agent compiles the assessment into a claim-ready package.
class ClaimDocumentGenerator:
XACTIMATE_CODES = {
"asphalt_shingle": {
"tear_off": "RFG TKOF",
"install": "RFG COMP",
"underlayment": "RFG FELT",
"flashing": "RFG FLSH",
"ridge_cap": "RFG RDGC",
"drip_edge": "RFG DRPE",
},
"metal": {
"tear_off": "RFG TKOF",
"install": "RFG MTL",
"underlayment": "RFG SYNT",
},
}
def generate_scope_of_work(self, assessment: RoofAssessment) -> dict:
codes = self.XACTIMATE_CODES.get(assessment.roof_type, {})
rec = assessment.recommendation()
line_items = []
if rec == "full_replacement":
sqft = assessment.total_sqft
line_items.extend([
{"code": codes.get("tear_off", ""), "description": "Tear off existing roofing",
"quantity": sqft, "unit": "SF"},
{"code": codes.get("install", ""), "description": f"Install {assessment.roof_type}",
"quantity": sqft, "unit": "SF"},
{"code": codes.get("underlayment", ""), "description": "Install underlayment",
"quantity": sqft, "unit": "SF"},
])
else:
for area in assessment.damage_areas:
line_items.append({
"code": codes.get("install", ""),
"description": f"Repair {area.location} — {area.damage_type.value}",
"quantity": area.size_sqft,
"unit": "SF",
})
# Add standard accessories
perimeter_lf = (assessment.total_sqft ** 0.5) * 4
line_items.append({
"code": codes.get("drip_edge", ""),
"description": "Install drip edge",
"quantity": round(perimeter_lf),
"unit": "LF",
})
return {
"recommendation": rec,
"line_items": line_items,
"total_sqft_affected": assessment.total_damage_sqft,
"photo_count": sum(len(a.photo_urls) for a in assessment.damage_areas),
}
def generate_claim_package(self, assessment: RoofAssessment) -> dict:
scope = self.generate_scope_of_work(assessment)
return {
"claim_type": "property_damage",
"date_of_loss": (
assessment.storm_date.strftime("%Y-%m-%d")
if assessment.storm_date else "Unknown"
),
"property_address": assessment.property_address,
"inspection_date": assessment.inspection_date.strftime("%Y-%m-%d"),
"roof_details": {
"type": assessment.roof_type,
"total_sqft": assessment.total_sqft,
"pitch": assessment.pitch,
"stories": assessment.stories,
},
"damage_summary": {
"areas_affected": len(assessment.damage_areas),
"total_damage_sqft": assessment.total_damage_sqft,
"damage_percentage": round(assessment.damage_percentage, 1),
"damage_types": list({a.damage_type.value for a in assessment.damage_areas}),
},
"scope_of_work": scope,
"supporting_documents": [
"Inspection photos",
"Measurement diagram",
"Storm date verification (weather report)",
"Material specification sheet",
],
}
Insurance Workflow Tracker
Roofing claims go through multiple stages with the insurance carrier. The agent tracks progress and prompts action.
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
class InsuranceWorkflowTracker:
WORKFLOW_STAGES = [
"claim_filed", "adjuster_assigned", "inspection_scheduled",
"inspection_complete", "estimate_received", "supplement_needed",
"supplement_submitted", "approved", "work_authorized",
]
def __init__(self, db):
self.db = db
async def update_claim_status(self, claim_id: str, new_status: str) -> dict:
current = await self.db.fetchrow(
"SELECT status, filed_date FROM insurance_claims WHERE claim_id = $1",
claim_id,
)
stage_index = self.WORKFLOW_STAGES.index(new_status)
next_action = self._get_next_action(new_status)
await self.db.execute(
"""UPDATE insurance_claims
SET status = $1, updated_at = NOW()
WHERE claim_id = $2""",
new_status, claim_id,
)
return {
"claim_id": claim_id,
"previous_status": current["status"],
"new_status": new_status,
"progress": f"{stage_index + 1}/{len(self.WORKFLOW_STAGES)}",
"next_action": next_action,
"days_since_filed": (datetime.now() - current["filed_date"]).days,
}
def _get_next_action(self, status: str) -> str:
actions = {
"claim_filed": "Wait for adjuster assignment (typical: 3-5 business days)",
"adjuster_assigned": "Contact adjuster to schedule inspection",
"inspection_scheduled": "Prepare for joint inspection — have documentation ready",
"inspection_complete": "Wait for carrier estimate (typical: 5-10 business days)",
"estimate_received": "Review estimate against your scope — prepare supplement if needed",
"supplement_needed": "Submit supplement with supporting documentation",
"supplement_submitted": "Follow up with adjuster in 7 business days",
"approved": "Send authorization form to homeowner for signature",
"work_authorized": "Schedule crew and order materials",
}
return actions.get(status, "Contact office for guidance")
Crew Scheduling for Roof Jobs
Roofing crews need specific equipment, favorable weather windows, and often work multiple jobs per week.
class RoofingCrewScheduler:
async def schedule_job(
self, job_id: str, assessment: RoofAssessment, weather_service,
) -> dict:
duration_days = self._estimate_duration(assessment)
min_crew_size = self._calculate_crew_size(assessment)
# Find weather-clear windows
forecast = await weather_service.get_extended_forecast(
assessment.property_address, days=14
)
clear_windows = [
day for day in forecast
if day["precipitation_chance"] < 20
and day["wind_speed_mph"] < 20
]
consecutive_clear = self._find_consecutive_days(clear_windows, duration_days)
if not consecutive_clear:
return {
"scheduled": False,
"reason": f"Need {duration_days} consecutive clear days — none found in 14-day forecast",
"next_check_date": forecast[-1]["date"],
}
return {
"scheduled": True,
"start_date": consecutive_clear[0]["date"],
"end_date": consecutive_clear[-1]["date"],
"crew_size": min_crew_size,
"duration_days": duration_days,
}
def _estimate_duration(self, assessment: RoofAssessment) -> int:
sqft = assessment.total_sqft if assessment.recommendation() == "full_replacement" else assessment.total_damage_sqft
sqft_per_day = 1500 if assessment.stories <= 1 else 1000
return max(1, round(sqft / sqft_per_day))
def _calculate_crew_size(self, assessment: RoofAssessment) -> int:
if assessment.total_sqft > 3000:
return 6
elif assessment.total_sqft > 1500:
return 4
return 3
def _find_consecutive_days(self, clear_days: list, needed: int) -> list:
for i in range(len(clear_days) - needed + 1):
window = clear_days[i:i + needed]
if len(window) == needed:
return window
return []
FAQ
How does the agent handle supplement negotiations with insurance carriers?
When the carrier's estimate is lower than the contractor's scope, the agent generates a supplement document that highlights specific line items where the carrier's pricing is below market rate or where damage areas were missed. It includes the relevant Xactimate codes, supporting photos for each disputed item, and references to the carrier's own pricing database. This structured approach increases supplement approval rates significantly compared to informal negotiations.
Can the agent verify storm dates against weather records?
Yes. The agent queries historical weather data APIs (NOAA Storm Events, Weather Underground) to verify that a hail or wind event occurred at the claimed location on the stated date. This verification is included in the claim package and strengthens the claim by providing independent corroboration of the date of loss.
What happens when a job needs to pause mid-project due to weather?
The agent monitors forecasts daily during active jobs. When rain is predicted, it alerts the crew lead to ensure tarps are properly secured on any open sections. It then recalculates the completion date and notifies the homeowner and any pending follow-on trades (gutters, siding) of the revised timeline.
#Roofing #DamageAssessment #InsuranceClaims #PhotoAnalysis #CrewScheduling #AgenticAI #LearnAI #AIEngineering
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.