Skip to content
Learn Agentic AI12 min read1 views

AI Agent for Moving Companies: Quote Generation, Inventory Tracking, and Day-of Coordination

Build an AI agent for moving companies that generates accurate quotes from room-by-room inventories, estimates cubic footage, assigns crews, and provides real-time customer updates on move day.

Why Moving Companies Need AI Agents

Moving companies operate on tight margins with intense customer anxiety. A customer calling for a quote wants an immediate, accurate price — but moving estimates depend on dozens of variables: number of rooms, heavy items (pianos, safes), flights of stairs, distance, packing services, and time of year. Underbidding leads to frustrated crews and cost overruns; overbidding loses the job to competitors. An AI agent that generates accurate quotes from structured inventory data, assigns the right crew size and truck, and keeps the customer informed on move day delivers a dramatically better experience.

The biggest source of customer complaints in the moving industry is surprises — unexpected costs, late arrivals, and damage. An AI agent eliminates surprises by setting accurate expectations upfront and providing real-time updates throughout the day.

Room-by-Room Inventory System

Accurate quotes start with accurate inventories. The agent walks customers through each room and calculates volume and weight.

from dataclasses import dataclass, field

@dataclass
class InventoryItem:
    name: str
    category: str
    cubic_feet: float
    weight_lbs: float
    requires_special_handling: bool = False
    requires_crating: bool = False
    disassembly_required: bool = False

STANDARD_ITEMS = {
    "king_bed": InventoryItem("King Bed", "bedroom", 70, 150, disassembly_required=True),
    "queen_bed": InventoryItem("Queen Bed", "bedroom", 60, 120, disassembly_required=True),
    "dresser_large": InventoryItem("Large Dresser", "bedroom", 35, 120),
    "sofa_3seat": InventoryItem("3-Seat Sofa", "living_room", 55, 200),
    "sofa_sectional": InventoryItem("Sectional Sofa", "living_room", 90, 350, requires_special_handling=True),
    "dining_table_6": InventoryItem("Dining Table (6-seat)", "dining", 35, 150, disassembly_required=True),
    "refrigerator": InventoryItem("Refrigerator", "kitchen", 45, 250, requires_special_handling=True),
    "washer": InventoryItem("Washer", "laundry", 30, 175, requires_special_handling=True),
    "dryer": InventoryItem("Dryer", "laundry", 30, 150),
    "piano_upright": InventoryItem("Upright Piano", "living_room", 40, 500, requires_special_handling=True, requires_crating=True),
    "piano_grand": InventoryItem("Grand Piano", "living_room", 80, 800, requires_special_handling=True, requires_crating=True),
    "boxes_small": InventoryItem("Small Box (1.5 cu ft)", "general", 1.5, 30),
    "boxes_medium": InventoryItem("Medium Box (3 cu ft)", "general", 3, 50),
    "boxes_large": InventoryItem("Large Box (4.5 cu ft)", "general", 4.5, 65),
}

@dataclass
class RoomInventory:
    room_name: str
    items: list[tuple[str, int]] = field(default_factory=list)  # (item_key, quantity)

    @property
    def total_cubic_feet(self) -> float:
        return sum(
            STANDARD_ITEMS[key].cubic_feet * qty
            for key, qty in self.items
            if key in STANDARD_ITEMS
        )

    @property
    def total_weight(self) -> float:
        return sum(
            STANDARD_ITEMS[key].weight_lbs * qty
            for key, qty in self.items
            if key in STANDARD_ITEMS
        )

class InventoryManager:
    def __init__(self):
        self.rooms: list[RoomInventory] = []

    def add_room(self, room_name: str, items: list[tuple[str, int]]) -> dict:
        room = RoomInventory(room_name=room_name, items=items)
        self.rooms.append(room)
        return {
            "room": room_name,
            "items_count": sum(qty for _, qty in items),
            "cubic_feet": round(room.total_cubic_feet, 1),
            "weight_lbs": round(room.total_weight, 0),
        }

    def get_full_inventory(self) -> dict:
        total_cf = sum(r.total_cubic_feet for r in self.rooms)
        total_wt = sum(r.total_weight for r in self.rooms)
        special_items = []
        for room in self.rooms:
            for key, qty in room.items:
                item = STANDARD_ITEMS.get(key)
                if item and (item.requires_special_handling or item.requires_crating):
                    special_items.append({
                        "item": item.name, "room": room.room_name,
                        "quantity": qty, "crating_needed": item.requires_crating,
                    })

        return {
            "rooms": len(self.rooms),
            "total_cubic_feet": round(total_cf, 1),
            "total_weight_lbs": round(total_wt, 0),
            "special_handling_items": special_items,
            "rooms_detail": [
                {"name": r.room_name, "cf": round(r.total_cubic_feet, 1)}
                for r in self.rooms
            ],
        }

Quote Generation Engine

The agent calculates pricing from inventory data, distance, and service options.

from datetime import datetime

class MoveQuoteGenerator:
    BASE_RATES = {
        "local": {"per_hour_2man": 120, "per_hour_3man": 165, "per_hour_4man": 210},
        "long_distance": {"per_mile": 0.85, "per_lb": 0.55},
    }
    TRUCK_SIZES = [
        {"name": "16ft", "capacity_cf": 800, "daily_rate": 75},
        {"name": "20ft", "capacity_cf": 1100, "daily_rate": 95},
        {"name": "26ft", "capacity_cf": 1700, "daily_rate": 130},
    ]
    PEAK_MONTHS = [5, 6, 7, 8, 9]
    PEAK_DAYS = [4, 5, 6]  # Friday, Saturday, Sunday

    def generate_quote(
        self, inventory: dict, distance_miles: float,
        origin_floors: int, destination_floors: int,
        packing_service: bool, move_date: datetime,
    ) -> dict:
        total_cf = inventory["total_cubic_feet"]
        total_wt = inventory["total_weight_lbs"]

        # Select truck
        truck = next(
            (t for t in self.TRUCK_SIZES if t["capacity_cf"] >= total_cf),
            self.TRUCK_SIZES[-1],
        )

        # Determine crew size
        if total_cf <= 600:
            crew_size = 2
        elif total_cf <= 1200:
            crew_size = 3
        else:
            crew_size = 4

        # Estimate hours for local moves
        base_hours = total_cf / 300  # ~300 cf per hour for loading
        stair_penalty = (max(0, origin_floors - 1) + max(0, destination_floors - 1)) * 0.5
        load_hours = base_hours + stair_penalty
        unload_hours = load_hours * 0.8
        drive_hours = distance_miles / 30
        total_hours = load_hours + drive_hours + unload_hours

        is_local = distance_miles <= 100
        if is_local:
            rate_key = f"per_hour_{crew_size}man"
            base_cost = total_hours * self.BASE_RATES["local"].get(rate_key, 210)
        else:
            base_cost = max(
                total_wt * self.BASE_RATES["long_distance"]["per_lb"],
                distance_miles * self.BASE_RATES["long_distance"]["per_mile"] * (total_wt / 1000),
            )

        # Add-ons
        packing_cost = total_cf * 1.5 if packing_service else 0
        special_handling = sum(
            150 if item.get("crating_needed") else 50
            for item in inventory.get("special_handling_items", [])
        )
        truck_cost = truck["daily_rate"]

        # Peak pricing
        peak_multiplier = 1.0
        if move_date.month in self.PEAK_MONTHS:
            peak_multiplier += 0.15
        if move_date.weekday() in self.PEAK_DAYS:
            peak_multiplier += 0.10

        subtotal = (base_cost + packing_cost + special_handling + truck_cost) * peak_multiplier
        insurance = subtotal * 0.03  # Basic valuation

        return {
            "move_type": "local" if is_local else "long_distance",
            "distance_miles": distance_miles,
            "total_cubic_feet": total_cf,
            "total_weight_lbs": total_wt,
            "truck": truck["name"],
            "crew_size": crew_size,
            "estimated_hours": round(total_hours, 1),
            "line_items": {
                "base_moving": round(base_cost, 2),
                "packing_service": round(packing_cost, 2),
                "special_handling": round(special_handling, 2),
                "truck_rental": truck_cost,
                "peak_adjustment": f"{(peak_multiplier - 1) * 100:.0f}%",
                "basic_insurance": round(insurance, 2),
            },
            "total_estimate": round(subtotal + insurance, 2),
            "binding_estimate": is_local is False,
            "valid_for_days": 14,
        }

Crew Assignment

The agent matches crews to jobs based on required skill sets, truck availability, and physical demands.

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

class CrewAssigner:
    def __init__(self, db):
        self.db = db

    async def assign_crew(
        self, move_date: datetime, crew_size: int,
        has_piano: bool, has_heavy_items: bool, truck_size: str,
    ) -> dict:
        required_skills = []
        if has_piano:
            required_skills.append("piano_certified")
        if has_heavy_items:
            required_skills.append("heavy_lift")

        available_movers = await self.db.fetch(
            """SELECT m.id, m.name, m.skills, m.truck_license,
                      m.rating, m.years_experience
               FROM movers m
               WHERE m.id NOT IN (
                   SELECT mover_id FROM assignments
                   WHERE move_date = $1
               )
               ORDER BY m.rating DESC""",
            move_date.date(),
        )

        qualified = [
            m for m in available_movers
            if all(skill in m["skills"] for skill in required_skills)
        ]
        if len(qualified) < crew_size:
            return {
                "assigned": False,
                "available": len(qualified),
                "needed": crew_size,
                "missing_skills": required_skills,
                "suggestion": "Consider alternate date or subcontracted crew",
            }

        crew_lead = qualified[0]
        crew_members = qualified[1:crew_size]

        # Check truck availability
        truck = await self.db.fetchrow(
            """SELECT truck_id, plate_number
               FROM trucks
               WHERE size = $1
                 AND truck_id NOT IN (
                     SELECT truck_id FROM assignments WHERE move_date = $2
                 )
               LIMIT 1""",
            truck_size, move_date.date(),
        )
        if not truck:
            return {"assigned": False, "reason": f"No {truck_size} truck available on {move_date.date()}"}

        return {
            "assigned": True,
            "crew_lead": {"name": crew_lead["name"], "experience_years": crew_lead["years_experience"]},
            "crew_members": [{"name": m["name"]} for m in crew_members],
            "truck": {"size": truck_size, "plate": truck["plate_number"]},
            "total_crew": crew_size,
        }

Day-of Customer Updates

On move day, the agent provides real-time status updates to the customer.

from datetime import datetime

class MoveDayCoordinator:
    def __init__(self, notification_service, tracking_service):
        self.notifier = notification_service
        self.tracking = tracking_service

    async def send_status_update(
        self, move_id: str, customer_phone: str, event: str,
    ) -> dict:
        status_messages = {
            "crew_dispatched": {
                "message": "Your moving crew has been dispatched and is on the way!",
                "include_eta": True,
            },
            "crew_arrived": {
                "message": "Your moving crew has arrived and is ready to begin.",
                "include_eta": False,
            },
            "loading_complete": {
                "message": "Loading is complete. The truck is heading to your new address.",
                "include_eta": True,
            },
            "arriving_destination": {
                "message": "The truck is 15 minutes away from your new address.",
                "include_eta": False,
            },
            "unloading_complete": {
                "message": "Unloading is complete! Please do a walkthrough to confirm all items.",
                "include_eta": False,
            },
        }

        status = status_messages.get(event)
        if not status:
            return {"error": f"Unknown event: {event}"}

        message = status["message"]
        if status["include_eta"]:
            eta = await self.tracking.get_eta(move_id)
            message += f" Estimated arrival: {eta}."

        await self.notifier.send_sms(to=customer_phone, message=message)

        await self.tracking.log_event(move_id, event, datetime.now())

        return {
            "move_id": move_id,
            "event": event,
            "message_sent": message,
            "timestamp": datetime.now().isoformat(),
        }

    async def handle_delay(
        self, move_id: str, customer_phone: str,
        reason: str, delay_minutes: int,
    ) -> dict:
        message = (
            f"Update on your move: We are running approximately "
            f"{delay_minutes} minutes behind schedule due to {reason}. "
            f"We apologize for the inconvenience and will keep you updated."
        )
        await self.notifier.send_sms(to=customer_phone, message=message)
        return {
            "move_id": move_id,
            "delay_minutes": delay_minutes,
            "reason": reason,
            "customer_notified": True,
        }

FAQ

How does the agent handle items not in the standard inventory list?

The agent allows customers to describe custom items by entering dimensions (length, width, height) and approximate weight. It calculates cubic footage from the dimensions and adds the item to the inventory with a "custom" category. For commonly added custom items, the system learns from historical data and can suggest adding them to the standard catalog.

Can the quote handle moves with multiple stops?

Yes. The agent supports multi-stop moves where items are picked up from one location and delivered to multiple addresses, or picked up from multiple origins. It calculates the routing, additional labor time at each stop, and adjusts the crew schedule accordingly. Each stop adds a minimum charge for the additional loading and unloading time.

How does the agent prevent damage claims?

Before the move, the agent generates a detailed inventory checklist with pre-existing condition notes. On move day, the crew lead marks each item as loaded. At delivery, the customer checks off each item on a digital manifest. Any discrepancy is flagged immediately rather than discovered days later. This digital chain of custody reduces disputed damage claims by 50-60% compared to paper-based systems.


#MovingCompanies #QuoteGeneration #InventoryTracking #CrewAssignment #CustomerCommunication #AgenticAI #LearnAI #AIEngineering

Share this article
C

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.