DEV Community

Richel E
Richel E

Posted on

How to Automate Contractor Scheduling, Quoting, and Follow-Up Using AI and APIs

Contractor businesses lose revenue in three places: missed calls that never get scheduled, quotes that take too long to send, and follow-ups that never happen because someone forgot. All three are fixable with AI and a handful of well-connected APIs.

This guide covers the architecture, code, and tools behind automating each workflow. It is written for developers building on field service platforms and technical operators deciding what to build versus buy.

What We Are Building

Direct answer: Three connected automation layers that handle scheduling, quoting, and follow-up without manual intervention.

Inbound Request (call, form, SMS)
        |
AI Qualification Layer
        |
Scheduling Engine (CRM API + Maps API)
        |
Quoting Engine (OpenAI + PDF + Email)
        |
CRM Job Created
        |
Webhook Events
        |
Follow-Up Sequence Router (SMS + Email + WhatsApp)
Enter fullscreen mode Exit fullscreen mode

Each layer connects via API. Human judgment is reserved for complex jobs. Everything repeatable runs automatically.

Why This Problem Is Worth Solving

Direct answer: Scheduling, quoting, and follow-up are repetitive, time-sensitive, and high-volume. That is exactly where automation delivers the fastest ROI.

Here is what manual looks like at a mid-sized HVAC or plumbing business:

  • A dispatcher spends 90 minutes every morning moving jobs around a calendar
  • An estimator takes 20 to 45 minutes per quote doing measurements, parts lookup, and document formatting
  • Follow-up messages go out only when someone remembers to send them

AI tools deliver 60% higher conversion rates through automated call handling and appointment scheduling. The architecture behind this is not complex. It is standard NLP, calendar APIs, and webhook-driven automation applied to workflows that have been manual by default.

Part 1: Automating Scheduling

How the Scheduling Engine Works

The engine does two things: it parses the inbound job request and assigns it to the best available technician based on weighted criteria.

Technician assignment criteria:

const assignmentCriteria = {
  location: {
    weight: 0.35,
    logic: "minimize_drive_time_from_current_location"
  },
  skill_match: {
    weight: 0.30,
    logic: "technician_certified_for_job_type"
  },
  availability: {
    weight: 0.25,
    logic: "no_overlap_in_calendar_window"
  },
  revenue_potential: {
    weight: 0.10,
    logic: "high_value_jobs_to_senior_techs"
  }
};
Enter fullscreen mode Exit fullscreen mode

Scheduling flow:

New job request received
        |
Parse job type and location
        |
Query technician availability via CRM API
        |
Score each available technician
        |
Assign highest-scoring technician
        |
Write appointment to CRM and calendar
        |
Send confirmation SMS to customer
Enter fullscreen mode Exit fullscreen mode

Housecall Pro API Integration

Housecall Pro exposes a REST API for reading technician schedules and writing jobs. Here is a minimal scheduling handler:

const scheduleJob = async (jobRequest) => {
  // Step 1: Get available technicians
  const techs = await fetch("https://api.housecallpro.com/employees", {
    headers: { "Authorization": `Token ${process.env.HCP_API_KEY}` }
  }).then(r => r.json());

  // Step 2: Filter and score
  const scored = techs
    .filter(t => t.skills.includes(jobRequest.jobType))
    .map(t => ({
      ...t,
      score: calculateScore(t, jobRequest, assignmentCriteria)
    }))
    .sort((a, b) => b.score - a.score);

  const assigned = scored[0];

  // Step 3: Create the job
  const job = await fetch("https://api.housecallpro.com/jobs", {
    method: "POST",
    headers: {
      "Authorization": `Token ${process.env.HCP_API_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      customer_id: jobRequest.customerId,
      job_type: jobRequest.jobType,
      scheduled_start: jobRequest.preferredTime,
      assigned_employee_id: assigned.id,
      notes: jobRequest.description
    })
  }).then(r => r.json());

  // Step 4: Confirm via SMS
  await sendSMS(
    jobRequest.customerPhone,
    `Confirmed: ${assigned.name} arrives at ${jobRequest.preferredTime}`
  );

  return job;
};
Enter fullscreen mode Exit fullscreen mode

Error Handling

Always handle no-availability and API failures gracefully:

try {
  const job = await scheduleJob(jobRequest);
  return { success: true, jobId: job.id };
} catch (err) {
  await notifyDispatcher({
    request: jobRequest,
    error: err.message,
    timestamp: new Date().toISOString()
  });
  return { success: false, fallback: "dispatcher_notified" };
}
Enter fullscreen mode Exit fullscreen mode

Tools for This Layer

Part 2: Automating Quoting

How the Quoting Pipeline Works

The pipeline takes an unstructured job description or photo as input and outputs a fully priced, formatted quote document ready to send.

Pipeline flow:

Input: job description or photo
        |
OpenAI extracts: job type, scope, materials, labor hours
        |
Lookup: material costs from price catalog
        |
Calculate: labor cost from saved rates
        |
Apply: markup and tax rules
        |
Generate: PDF quote document
        |
Deliver: via email and SMS
Enter fullscreen mode Exit fullscreen mode

Step 1: AI Extraction With OpenAI

const extractJobData = async (description) => {
  const res = await openai.chat.completions.create({
    model: "gpt-4o",
    messages: [
      {
        role: "system",
        content: `You are a field service estimator.
        Extract the following from the job description.
        Return JSON only. No explanation.
        {
          "job_type": string,
          "scope": "minor" | "standard" | "major",
          "materials": [{ "item": string, "quantity": number, "unit": string }],
          "estimated_labor_hours": number,
          "urgency": "standard" | "emergency"
        }`
      },
      {
        role: "user",
        content: description
      }
    ]
  });

  return JSON.parse(res.choices[0].message.content);
};
Enter fullscreen mode Exit fullscreen mode

Keep the system prompt short and specific. Long prompts with many edge cases produce inconsistent JSON output. Validate the schema before passing data downstream.

Step 2: Build the Quote

const buildQuote = async (jobData) => {
  const catalog = await getMaterialCatalog();
  const laborRate = await getLaborRate(jobData.job_type);

  const materialCosts = jobData.materials.map(m => {
    const item = catalog.find(c => c.item === m.item);
    return {
      ...m,
      unit_cost: item?.price || 0,
      total: (item?.price || 0) * m.quantity
    };
  });

  const materialTotal = materialCosts.reduce((s, m) => s + m.total, 0);
  const laborTotal = jobData.estimated_labor_hours * laborRate;
  const subtotal = materialTotal + laborTotal;
  const markup = subtotal * 0.20;
  const tax = subtotal * 0.08;
  const urgencySurcharge = jobData.urgency === "emergency"
    ? subtotal * 0.25
    : 0;

  return {
    line_items: materialCosts,
    labor: { hours: jobData.estimated_labor_hours, rate: laborRate, total: laborTotal },
    subtotal,
    markup,
    tax,
    urgency_surcharge: urgencySurcharge,
    total: subtotal + markup + tax + urgencySurcharge
  };
};
Enter fullscreen mode Exit fullscreen mode

Step 3: Deliver the Quote

const deliverQuote = async (quote, customer) => {
  const pdf = await generatePDF(quote, customer);

  await sendEmail({
    to: customer.email,
    subject: "Your estimate is ready",
    attachment: pdf,
    body: `Hi ${customer.firstName}, your estimate for
           ${quote.job_type} is attached.
           Total: $${quote.total}.
           Reply to approve or call us with questions.`
  });

  await sendSMS(
    customer.phone,
    `Your estimate: $${quote.total}. Approve here: ${quote.approvalUrl}`
  );
};
Enter fullscreen mode Exit fullscreen mode

AI is being applied to cost estimation by 24% of contractors and bid management by 22%, making quoting one of the fastest growing automation use cases in the trades right now.

Tools for This Layer

Part 3: Automating Follow-Up

How Follow-Up Sequences Work

Follow-up runs on webhook events from the CRM. When a job status changes, it triggers the corresponding sequence automatically.

Event to sequence mapping:

CRM Webhook Event
        |
[ job.completed | estimate.sent | job.scheduled | anniversary_date ]
        |
Sequence Router
        |
[ post_job_review | estimate_nudge | pre_job_reminder | re_engagement ]
        |
Delivery Layer
        |
[ Twilio SMS | SendGrid Email | WhatsApp via 360dialog ]
Enter fullscreen mode Exit fullscreen mode

Webhook Handler

app.post("/webhook/hcp", async (req, res) => {
  const { event_type, job } = req.body;

  switch (event_type) {
    case "job.completed":
      await triggerSequence("post_job_review", job);
      break;
    case "estimate.sent":
      await scheduleFollowUp("estimate_no_response", job, "24h");
      break;
    case "job.scheduled":
      await triggerSequence("pre_job_reminder", job);
      break;
    default:
      console.log(`Unhandled event: ${event_type}`);
  }

  res.sendStatus(200);
});
Enter fullscreen mode Exit fullscreen mode

Sequence Definitions

const sequences = {
  post_job_review: [
    { delay: "2h", channel: "sms",   template: "review_request" },
    { delay: "7d", channel: "email", template: "satisfaction_followup" }
  ],
  estimate_no_response: [
    { delay: "24h", channel: "sms",   template: "estimate_followup_1" },
    { delay: "72h", channel: "email", template: "estimate_followup_2" },
    { delay: "7d",  channel: "sms",   template: "estimate_final" }
  ],
  pre_job_reminder: [
    { delay: "24h", channel: "sms", template: "appointment_reminder" },
    { delay: "-2h", channel: "sms", template: "technician_on_way" }
  ],
  annual_maintenance: [
    { delay: "365d", channel: "email", template: "maintenance_reminder" },
    { delay: "367d", channel: "sms",   template: "maintenance_sms" }
  ]
};
Enter fullscreen mode Exit fullscreen mode

Dynamic Message Templates

const templates = {
  review_request: (job) =>
    `Hi ${job.customer.first_name}, thanks for choosing us today.
    We would love your feedback: ${process.env.REVIEW_URL}`,

  estimate_followup_1: (job) =>
    `Hi ${job.customer.first_name}, following up on your estimate
    for ${job.job_type} totaling $${job.estimate_total}.
    Any questions? Reply here or call us.`,

  maintenance_reminder: (job) =>
    `Hi ${job.customer.first_name}, it has been a year since your
    ${job.job_type} service. Time for your annual check.
    Book here: ${process.env.BOOKING_URL}`
};
Enter fullscreen mode Exit fullscreen mode

Opt-Out Handling

Every sequence needs opt-out logic. Never skip this.

const isOptedOut = async (phone) => {
  const record = await db.optouts.findOne({ phone });
  return !!record;
};

const sendSequenceMessage = async (message, recipient) => {
  if (await isOptedOut(recipient.phone)) return;
  await sendSMS(recipient.phone, message);
};
Enter fullscreen mode Exit fullscreen mode

Tools for This Layer

  • Housecall Pro Webhooks — trigger events on job status changes
  • BullMQ or Agenda.js — delayed job queue for scheduled messages
  • Redis — queue backing store
  • Twilio SMS API — SMS delivery
  • SendGrid API — email delivery
  • WhatsApp Business API via 360dialog — compliant after-hours messaging

Full Stack and Monthly Cost

Component Tool Monthly Cost
Scheduling and CRM Housecall Pro $49 to $149
AI extraction OpenAI GPT-4o API $20 to $40
Quote delivery SendGrid + PDFKit $15 to $20
Follow-up SMS Twilio SMS $20 to $40
WhatsApp sequences 360dialog $10 to $20
Queue and delayed jobs Redis + BullMQ $10 to $15
Workflow automation Make or Zapier $20 to $45
Total $144 to $329/month

At an average job value of $400, recovering two missed bookings per month covers the entire stack.

Common Mistakes to Avoid

Not handling CRM API rate limits

Housecall Pro and most field service APIs enforce rate limits. Implement exponential backoff on all API calls:

const fetchWithRetry = async (url, options, retries = 3) => {
  try {
    return await fetch(url, options);
  } catch (err) {
    if (retries > 0) {
      await new Promise(r =>
        setTimeout(r, 2 ** (3 - retries) * 1000)
      );
      return fetchWithRetry(url, options, retries - 1);
    }
    throw err;
  }
};
Enter fullscreen mode Exit fullscreen mode

Over-engineering the AI extraction prompt

Keep the OpenAI system prompt minimal. A short, specific prompt with a clean JSON schema outperforms a long prompt with many edge cases. Validate output schema before passing data downstream.

Skipping service area validation

Geocode every address against your service area polygon before confirming a booking. Out-of-area jobs that slip into the CRM waste technician time and create bad customer experiences.

Building follow-up last

Most teams build the capture layer first and treat follow-up as a phase two task. This is backwards. Re-engaging past customers delivers faster ROI than acquiring new ones for most field service businesses. Build follow-up sequences in month one, not month three.

FAQ

Which field service CRMs have the best API coverage?

Housecall Pro and Jobber are the most accessible for small and mid-market contractors. Both have well-documented REST APIs, webhook support, and sandbox environments. ServiceTitan has deeper coverage but requires enterprise access and a longer onboarding process.

Can the quoting engine handle photo input?

Yes. Pass the image as a base64-encoded string to GPT-4o's vision input. The model extracts visible scope, materials, and damage from job site photos with reasonable accuracy. Validate AI estimates against historical job data before deploying to production.

What is the best queue system for delayed follow-up messages?

BullMQ with Redis is the most reliable option for Node.js. For lower-volume operations, Make.com or Zapier with built-in delay steps handles most contractor follow-up sequences without custom infrastructure.

How do you handle emergency jobs that bypass automation?

Classify urgency in the inbound qualification step. When the AI detects emergency keywords, route the job to a warm transfer or immediate dispatcher notification rather than the standard automated booking flow.

Is WhatsApp Business API compliant for contractor messaging?

Yes, when using approved partners like Twilio or 360dialog. Keep messages within WhatsApp's 24-hour window for free-form messages. Use pre-approved message templates for messages outside that window.

Summary

Automating scheduling, quoting, and follow-up for contractor businesses comes down to three connected layers:

  • A scheduling engine that scores and assigns technicians via weighted criteria and CRM API calls
  • A quoting pipeline that extracts job data with OpenAI and outputs formatted, priced documents automatically
  • A webhook-driven follow-up system that triggers sequences based on CRM job status events

The stack runs at $144 to $329 per month. The APIs are well documented. The integration points are straightforward. The biggest leverage is connecting all three so data moves once and updates everywhere, with human judgment reserved only for jobs that actually need it.

Building automation for contractor businesses or field service platforms? Share what you are working on in the comments.

Top comments (0)