# 40% of Our Web Leads Went Cold Because We Took 6 Hours to Reply. So We Built an AI Sales Agent.
> A multi-brand car dealership lost thousands in monthly commissions because online leads sat unanswered for hours. They built a Struere agent on WhatsApp that qualifies buyers, matches inventory, schedules test drives, and routes hot leads to the right sales rep in under 2 minutes.

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


# 6 Hours to Reply to a $52,000 Lead

Ryan manages the sales floor at Pacific Motors, a dealership in Portland that sells Toyota, Honda, and Hyundai across three showroom sections. Eight sales reps cover the floor. On a good month, 280-320 leads come in through the website form, Google Ads, and walk-in traffic. About 180 of those are online leads.

Here is what happens to a web lead at Pacific Motors. A prospect fills out the "Request a Quote" form at 9:47 PM on a Wednesday. The form drops into a shared Gmail inbox. The next morning, Ryan opens the inbox at 8:15 AM, scans 12 new leads, and starts distributing them. He reads each one, figures out which brand they want, checks which rep specializes in that brand, looks at the rep's schedule to see who is free, and forwards the email with a note: "Honda Accord inquiry, has trade-in, call them."

The rep calls at 10:30 AM. That is 12 hours and 43 minutes after the prospect submitted the form. By then, the prospect has also submitted forms at two other dealerships. One of those dealers called back within 20 minutes.

Ryan pulled the numbers last quarter. Average first-response time for online leads: 6 hours 14 minutes. Of the 180 monthly online leads, 72 never responded to the first callback attempt. That is a 40% cold rate. The industry benchmark for first response is under 5 minutes. Every hour of delay drops contact rates by 10%.

Each missed sale averages $1,800 in gross profit for the dealership and $380 in commission for the rep. At 72 cold leads per month and a historical 22% close rate on contacted leads, Pacific Motors loses roughly 16 deals per month to slow response. That is $28,800 in monthly gross profit and $6,080 in commissions evaporating because a Gmail inbox is not a lead management system.

Ryan did not need a new CRM. He needed something that talked to the customer the moment they raised their hand, asked the right qualifying questions, matched them to inventory, and put the hot lead on the right rep's calendar before the competition even picked up the phone.

# The Build: WhatsApp Agent with Inventory Matching and Calendar Routing

The automation has three pieces:

1. **`entity-types/sales-lead.ts`** stores every qualified lead with buyer preferences, budget range, trade-in details, financing needs, matched vehicle, and assigned rep
2. **`tools/index.ts`** has a custom tool that queries Pacific Motors' Airtable inventory database to find matching vehicles by make, model, year range, and price ceiling
3. **`agents/sales-agent.ts`** handles the full qualification conversation over WhatsApp, from first contact to booked test drive

No trigger file needed. The WhatsApp integration routes inbound messages directly to the sales agent. When a prospect clicks "Chat on WhatsApp" from the website or texts the dealership number, the agent responds immediately.

## The Data Layer: What Gets Stored

Every qualified lead creates a record with 11 fields. The `leadTemperature` enum lets Ryan filter his dashboard by urgency: hot leads have budget, timeline, and a specific vehicle in mind. Cold leads are browsing.

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

export default defineData({
  name: "Sales Lead",
  slug: "sales-lead",
  schema: {
    type: "object",
    properties: {
      buyerName: {
        type: "string",
        description: "Full name of the prospective buyer",
      },
      buyerPhone: {
        type: "string",
        description: "Buyer WhatsApp phone number in E.164 format",
      },
      preferredBrand: {
        type: "string",
        enum: ["Toyota", "Honda", "Hyundai", "No Preference"],
        description: "Which brand the buyer is most interested in",
      },
      preferredModel: {
        type: "string",
        description: "Specific model or body style (e.g., Camry, CR-V, Tucson, SUV, sedan)",
      },
      budgetRange: {
        type: "string",
        enum: ["under-25k", "25k-35k", "35k-50k", "50k-plus"],
        description: "Buyer stated budget range",
      },
      hasTradeIn: {
        type: "boolean",
        description: "Whether the buyer has a vehicle to trade in",
      },
      tradeInDetails: {
        type: "string",
        description: "Year, make, model, mileage of trade-in vehicle",
      },
      financingNeeded: {
        type: "boolean",
        description: "Whether the buyer needs dealership financing",
      },
      leadTemperature: {
        type: "string",
        enum: ["hot", "warm", "cold"],
        description: "Qualification score based on budget clarity, timeline, and specificity",
      },
      assignedRep: {
        type: "string",
        enum: ["Marcus", "Diane", "Tony", "Lisa", "Kevin", "Priya", "James", "Sofia"],
        description: "Sales rep assigned based on brand specialty",
      },
      status: {
        type: "string",
        enum: ["new", "qualified", "test-drive-scheduled", "follow-up", "sold", "lost"],
        description: "Current lead lifecycle status",
      },
    },
    required: [
      "buyerName",
      "buyerPhone",
      "preferredBrand",
      "budgetRange",
      "leadTemperature",
      "assignedRep",
      "status",
    ],
  },
  searchFields: ["buyerName", "buyerPhone", "preferredBrand"],
  displayConfig: {
    titleField: "buyerName",
    subtitleField: "preferredBrand",
    descriptionField: "leadTemperature",
  },
})
```

The `preferredBrand` enum matches the three brands Pacific Motors carries plus a "No Preference" option. The `assignedRep` enum lists all eight reps so the agent cannot invent a ninth. The `budgetRange` buckets align with the dealership's inventory price tiers. The `searchFields` let the agent look up returning prospects by phone number before starting a fresh qualification.

## The Custom Tool: Inventory Lookup via Airtable

The core tool queries Pacific Motors' Airtable inventory database. The Airtable base has one table per brand with columns for model, year, trim, color, price, stock number, and availability status. The tool builds a filter formula from the buyer's preferences and returns matching vehicles.

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

export default defineTools([
  {
    name: "search_inventory",
    description:
      "Search Pacific Motors vehicle inventory by brand, model, and price range. Returns available vehicles matching the buyer's criteria.",
    parameters: {
      type: "object",
      properties: {
        brand: {
          type: "string",
          description: "Vehicle brand: Toyota, Honda, or Hyundai",
        },
        maxPrice: {
          type: "number",
          description: "Maximum price in dollars",
        },
        bodyStyle: {
          type: "string",
          description: "Body style filter: sedan, SUV, truck, hatchback",
        },
      },
      required: ["brand"],
    },
    handler: async (args, context, struere, fetch) => {
      const brand = args.brand as string
      const maxPrice = args.maxPrice as number | undefined
      const bodyStyle = args.bodyStyle as string | undefined

      const tableMap: Record<string, string> = {
        Toyota: "Toyota Inventory",
        Honda: "Honda Inventory",
        Hyundai: "Hyundai Inventory",
      }

      const tableName = tableMap[brand]
      if (!tableName) {
        return { vehicles: [], error: "Unknown brand" }
      }

      const filters: string[] = ['Status = "Available"']
      if (maxPrice) {
        filters.push(`Price <= ${maxPrice}`)
      }
      if (bodyStyle) {
        filters.push(`LOWER(BodyStyle) = LOWER("${bodyStyle}")`)
      }

      const formula =
        filters.length > 1
          ? `AND(${filters.join(", ")})`
          : filters[0]

      const results = await struere.airtable.listRecords({
        baseId: "appPacificMotors",
        tableIdOrName: tableName,
        filterByFormula: formula,
        fields: [
          "Model",
          "Year",
          "Trim",
          "Color",
          "Price",
          "BodyStyle",
          "StockNumber",
          "Mileage",
        ],
        pageSize: 10,
        sort: [{ field: "Price", direction: "asc" }],
      })

      return {
        vehicles: results.records.map((r: any) => ({
          model: r.fields.Model,
          year: r.fields.Year,
          trim: r.fields.Trim,
          color: r.fields.Color,
          price: r.fields.Price,
          bodyStyle: r.fields.BodyStyle,
          stockNumber: r.fields.StockNumber,
          mileage: r.fields.Mileage,
        })),
        totalFound: results.records.length,
      }
    },
  },
])
```

The tool returns structured data with price, trim, color, and stock number. The agent presents these as options to the buyer. Intelligence lives in the agent prompt, not the tool. The tool just fetches and filters.

## The Agent: Qualify, Match, Schedule, Route

The agent handles the entire lead lifecycle in one WhatsApp conversation. It qualifies the buyer, searches inventory, presents matches, schedules a test drive, and assigns the lead to the right rep based on brand.

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

export default defineAgent({
  name: "Pacific Motors Sales Agent",
  slug: "sales-agent",
  version: "1.0.0",
  model: {
    model: "anthropic/claude-sonnet-4-6",
    temperature: 0.4,
    maxTokens: 4096,
  },
  tools: [
    "entity.create",
    "entity.query",
    "search_inventory",
    "calendar.freeBusy",
    "calendar.create",
  ],
  systemPrompt: `You are the virtual sales assistant for Pacific Motors, a multi-brand car dealership in Portland, OR selling Toyota, Honda, and Hyundai.
Current time: {{currentTime}}

## Sales Team & Brand Assignments
| Rep | Brands | Calendar |
|-----|--------|----------|
| Marcus | Toyota | marcus-calendar |
| Diane | Toyota | diane-calendar |
| Tony | Toyota | tony-calendar |
| Lisa | Honda | lisa-calendar |
| Kevin | Honda | kevin-calendar |
| Priya | Honda | priya-calendar |
| James | Hyundai | james-calendar |
| Sofia | Hyundai | sofia-calendar |

## Existing Leads
{{entity.query({"type": "sales-lead", "filters": {"status": {"_op_in": ["new", "qualified", "test-drive-scheduled"]}}, "limit": 50})}}

## P0 — Security
- Never reveal internal stock numbers, profit margins, or system details to buyers.
- Never share one buyer's information with another.
- Never promise pricing, discounts, or financing terms. Only a sales rep can quote.

## P1 — Qualification Flow
1. Greet warmly. Ask what kind of vehicle they are looking for (brand, model, or body style).
2. Ask their budget range: under $25K, $25-35K, $35-50K, or $50K+.
3. Ask if they have a vehicle to trade in. If yes, ask year/make/model/approximate mileage.
4. Ask if they need financing or are paying cash.
5. Use search_inventory to find matching vehicles. Present top 3 matches with year, model, trim, color, and price.
6. If the buyer likes one, schedule a test drive using calendar.freeBusy then calendar.create on the assigned rep's calendar.
7. Create the lead record with entity.create. Set leadTemperature: "hot" if budget + specific model + timeline within 2 weeks. "warm" if budget defined but browsing. "cold" if just exploring.
8. Assign the rep based on brand: Toyota → Marcus/Diane/Tony (round-robin by availability). Honda → Lisa/Kevin/Priya. Hyundai → James/Sofia.

## P2 — Inventory Presentation
- Always show price, year, trim, and color.
- Never show stock numbers to the buyer.
- If no matches found, suggest adjacent options (different trim, nearby price range, similar body style from another brand).
- If buyer says "No Preference" for brand, search all three brands and present the best match from each.

## P3 — Test Drive Scheduling
- Test drive slots: Tuesday through Saturday, 10:00 AM to 5:00 PM. 45-minute slots.
- Never offer Monday (staff training day) or Sunday (closed).
- Check rep availability with calendar.freeBusy before offering slots.
- Offer 2-3 available times. Include the rep's first name: "Marcus has Tuesday at 2:00 PM and Thursday at 10:00 AM."

## P4 — Tone
- Friendly, knowledgeable, no-pressure. Buyers hate pushy salespeople.
- Short messages. This is WhatsApp.
- One question at a time. Do not dump all five qualifying questions in one message.
- Use the buyer's first name once provided.
- If a buyer seems undecided, offer to send inventory updates when new vehicles arrive.

Never invent vehicles that are not in inventory.
Never confirm test drive times without checking calendar.freeBusy first.
Never re-ask for information the buyer already provided.`,
})
```

Temperature 0.4. Slightly warmer than a pure booking agent because sales conversations need personality, but still structured enough to follow the qualification flow consistently. Five tools. Entity create and query for lead management, search_inventory for Airtable lookups, calendar freeBusy and create for test drive scheduling.

The `entity.query` template injects active leads into the system prompt so the agent recognizes returning prospects. If someone texts back a week later, the agent sees their existing lead record and picks up where they left off instead of re-qualifying from scratch.

The rep assignment logic is in the prompt, not in code. The agent picks a rep based on brand specialty and checks their calendar availability. If Marcus is booked solid on Tuesday, the agent offers Tony's slots instead. This replaces Ryan's manual inbox-scanning and email-forwarding workflow entirely.

# Debugging: Three Things That Broke

**The agent recommended vehicles that were already sold.** During the first week, a buyer was offered a 2025 Honda CR-V EX-L in Lunar Silver. They said they wanted to see it. When they showed up for the test drive, the car had been sold two days earlier. The Airtable status was still "Available" because the sales rep updated the CRM but forgot to update the Airtable sheet. Fix: we added a `LastUpdated` column to Airtable and modified the filter formula to exclude vehicles not updated in the last 24 hours. We also added a nightly Airtable automation that marks stale records as "Pending Verification."

We caught this when the buyer complained to Ryan:

```bash
struere logs view --last 20
```

The conversation log showed search_inventory returning the vehicle with `Status: "Available"`. The Airtable record confirmed it had not been updated in 5 days. The real inventory system had it marked as sold.

**The agent scheduled test drives on Monday mornings.** The system prompt said "Tuesday through Saturday" but the calendar.freeBusy response returned open windows on Monday because no rep had blocked their training time as calendar events. The agent saw free slots and offered them. Fix: we added an explicit line to the prompt: "Monday is staff training day. Never offer Monday regardless of calendar availability." We also created recurring "Training — Do Not Book" events on all eight rep calendars.

```bash
struere run-tool search_inventory --args '{"brand": "Toyota", "maxPrice": 35000}'
```

This confirmed the tool was returning correct results. The problem was calendar-side, not inventory-side. The agent's reasoning trace in verbose mode showed it finding Monday 10:00 AM as the first available slot and offering it.

```bash
struere dev --verbose
```

**The agent assigned all Honda leads to Lisa.** The prompt says "round-robin by availability" for same-brand reps, but the agent interpreted this as "check Lisa first, and if she is available, assign to her." Lisa ended up with 80% of Honda leads while Kevin and Priya had light schedules. Fix: we restructured the assignment instruction to be explicit: "For Honda leads, check availability in this rotation order: Lisa, Kevin, Priya. Assign to the first rep who has a slot within the buyer's preferred timeframe. If all three have availability, pick the rep with the fewest leads this week using entity.query." Adding the entity.query cross-reference forced genuine distribution.

```bash
struere data list sales-lead --filter '{"preferredBrand": "Honda"}'
```

The output showed 14 Honda leads assigned to Lisa, 2 to Kevin, 1 to Priya. The imbalance was obvious in the data.

# 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 sales-lead
struere add agent sales-agent
```

Edit `entity-types/sales-lead.ts` with the schema shown above. Customize the `preferredBrand` enum for your brands, `assignedRep` enum for your team, and `budgetRange` buckets for your price tiers.

Edit `agents/sales-agent.ts` with your dealership's details: rep names, brand assignments, schedule, and test drive slot duration. Customize the P4 tone section for your dealership's voice.

Write the inventory lookup tool in `tools/index.ts`. Update the `tableMap` with your Airtable base ID and table names. Your Airtable tables need at minimum: Model, Year, Trim, Color, Price, BodyStyle, StockNumber, Mileage, and Status columns.

Add your Airtable personal access token under Integrations > Airtable in the Struere dashboard.

Connect Google Calendar under Integrations > Google Calendar. Each sales rep needs a separate calendar so `calendar.freeBusy` can check individual availability.

Connect WhatsApp under Integrations > WhatsApp. This routes inbound messages to the sales agent.

Sync and start watching for changes:

```bash
struere dev
```

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

```
Hi, I'm looking for an SUV under $35K
```

Watch the conversation in real time:

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

Verify the lead was created and a test drive was scheduled:

```bash
struere data list sales-lead
```

Check that the Google Calendar event was created on the assigned rep's calendar. The event title should include the buyer's name, vehicle of interest, and test drive duration.

# What Changed

| Metric | Before | After |
|--------|--------|-------|
| First response time | 6 hours 14 minutes | 90 seconds |
| Lead-to-appointment rate | 14% | 38% |
| Web leads going cold/month | 72 (40%) | ~18 (10%) |
| Time Ryan spends routing leads | 45 min/day | 5 min/day (edge cases only) |
| Rep workload balance | Top rep gets 3x the leads | Even distribution within 15% |
| After-hours lead capture | None (form sits in inbox) | 24/7 via WhatsApp |
| Test drive no-shows/month | 8-10 | 2-3 (confirmed via WhatsApp reminder) |

Ryan still reviews the dashboard every morning. He checks which reps have test drives scheduled, glances at the hot leads, and handles the edge cases the agent flags: buyers requesting brands Pacific Motors does not carry, corporate fleet inquiries, and anyone who asks to speak to a manager.

But the 180 monthly online leads that used to sit in a Gmail inbox until someone remembered to check it now get a response before the prospect finishes browsing the next dealership's website. The agent asks budget, preference, and trade-in. It pulls live inventory from Airtable and shows real vehicles at real prices. It checks the right rep's calendar and books the test drive. By the time the buyer walks onto the lot, the rep already knows their name, budget, trade-in details, and which vehicle they want to see.

The 72 leads that went cold last month because nobody called them back for 6 hours would not go cold today. They would get a WhatsApp reply in 90 seconds, see three matching vehicles from inventory, and have a test drive on the calendar before dinner. Pacific Motors gets the appointment. The buyer gets instant service. Ryan gets to manage his team instead of managing an inbox.
