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)
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"
}
};
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
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;
};
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" };
}
Tools for This Layer
- Housecall Pro API — job creation and technician management
- Google Maps Distance Matrix API — drive time calculation for location scoring
- Twilio SMS API — booking confirmation messages
- BuildOps — AI-native alternative that unifies scheduling, dispatching, and invoicing with OpsAI built in
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
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);
};
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
};
};
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}`
);
};
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
- OpenAI GPT-4o API — job data extraction from text or image
- BuildOps OpsAI — converts purchase orders into accurate invoice line items automatically
- PDFKit or Puppeteer — quote PDF generation
- SendGrid API — email delivery
- Twilio API — SMS delivery with approval link
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 ]
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);
});
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" }
]
};
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}`
};
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);
};
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;
}
};
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)