Expert guide
HVAC Maintenance Agreement Tracker
Every paid visit scheduled, every renewal on time, every worn-out unit turned into a quote
Call us at +1 (385) 386-3860
Short answer: store customers, sites, equipment, agreements and visits as related tables (Access is ideal), record visit frequency on each agreement, and run a seasonal routine that creates due visits for every active agreement. Track visit completion, renewal dates 60 days ahead, and equipment with repeated issues as replacement leads. Our work order management Access database is a ready starting point.
Maintenance agreements are the most valuable revenue an HVAC company has: predictable, paid in advance, and the main source of repair and replacement work. They are also easy to mismanage. Spring tune-ups get booked for whoever calls first, autumn visits for some customers never happen, and renewals lapse because nobody noticed the date. Customers who paid for two visits and got one do not renew.
Why a Spreadsheet Struggles Here
One customer can have several sites; one site can have several units; one agreement covers some of those units and includes two visits a year; each visit has a technician, date and findings. A single spreadsheet row per customer cannot hold that without duplicate rows or dozens of columns. For a small book Excel can cope, but the structure below in Access scales to thousands of agreements.
The Tables
| Table | Key fields |
|---|---|
| tblCustomers | CustomerID, Name, BillingAddress, Phone, Email |
| tblSites | SiteID, CustomerID, Address, AccessNotes |
| tblEquipment | EquipID, SiteID, Type, Make, Model, Serial, InstallYear, Refrigerant |
| tblAgreements | AgreementID, CustomerID, Plan, StartDate, EndDate, Price, VisitsPerYear, Season pattern, AutoRenew, Status |
| tblAgreementEquipment | AgreementID, EquipID (which units are covered) |
| tblVisits | VisitID, AgreementID, SiteID, Season, DueFrom, DueBy, ScheduledOn, Technician, CompletedOn, Findings |
| tblFindings | FindingID, VisitID, EquipID, Issue, Severity, RecommendedAction, Quoted |
Generating Seasonal Visits Automatically
-- Create spring visits for active agreements that include spring
-- and do not yet have one for this year (run in February)
INSERT INTO tblVisits (AgreementID, SiteID, Season, DueFrom, DueBy)
SELECT a.AgreementID, s.SiteID, "Spring 2027",
#2027-03-01#, #2027-05-31#
FROM (tblAgreements AS a
INNER JOIN tblSites AS s ON a.CustomerID = s.CustomerID)
WHERE a.Status = "Active"
AND a.EndDate >= #2027-03-01#
AND a.SeasonPattern In ("Spring+Autumn", "Spring")
AND NOT EXISTS (SELECT 1 FROM tblVisits AS v
WHERE v.AgreementID = a.AgreementID
AND v.SiteID = s.SiteID
AND v.Season = "Spring 2027");The NOT EXISTS clause makes the routine safe to run twice. In practice, wrap this in a form where the office manager picks the season and dates, so nobody edits SQL each year.
Dispatch: Working the Due List
Unscheduled visits for the season, sorted by area or postcode for efficient routing.
Customers with access notes or special requirements highlighted.
A button to create the work order from the visit, carrying site and equipment details.
Overdue visits (past DueBy and not completed) on a red list reviewed weekly.
If you already use a work order system, visits become work orders of type "PM". The work order management database shows that pattern.
Renewal Pipeline
qryRenewalsDue:
SELECT a.AgreementID, c.Name, a.Plan, a.EndDate, a.Price,
DateDiff("d", Date(), a.EndDate) AS DaysLeft,
(SELECT Count(*) FROM tblVisits v WHERE v.AgreementID = a.AgreementID
AND v.CompletedOn Is Not Null) AS VisitsDone,
a.VisitsPerYear
FROM tblAgreements AS a INNER JOIN tblCustomers AS c ON a.CustomerID = c.CustomerID
WHERE a.Status = "Active" AND a.EndDate Between Date() And Date() + 60
ORDER BY a.EndDate;Showing VisitsDone next to VisitsPerYear before calling a customer about renewal is important. If a visit was missed, schedule it or offer a credit first; asking for renewal money from a customer who did not get their visit is how agreements are lost.
Turning Findings Into Replacement Leads
Record findings per unit with severity, not just in free-text notes.
Flag units older than a set age with repeated findings as replacement candidates.
Track whether each recommended action was quoted, and the outcome.
Report monthly: replacement candidates by technician and area for the sales team.
SELECT e.EquipID, c.Name, e.Make, e.Model, e.InstallYear,
Count(f.FindingID) AS IssuesLast2Years
FROM (((tblFindings AS f
INNER JOIN tblVisits AS v ON f.VisitID = v.VisitID)
INNER JOIN tblEquipment AS e ON f.EquipID = e.EquipID)
INNER JOIN tblSites AS s ON e.SiteID = s.SiteID)
INNER JOIN tblCustomers AS c ON s.CustomerID = c.CustomerID
WHERE f.Severity In ("Major", "Critical")
AND v.CompletedOn >= DateAdd("yyyy", -2, Date())
GROUP BY e.EquipID, c.Name, e.Make, e.Model, e.InstallYear
HAVING Count(f.FindingID) >= 2
ORDER BY e.InstallYear;Where AI Helps
Turn technician shorthand into a clear visit summary for the customer.
Classify free-text findings into issue type and severity for reporting.
Draft renewal reminders that mention the visits completed and any recommendations.
Summarise a unit's service history before a replacement conversation.
The integration pattern is in connecting MS Access to the OpenAI API. Technicians' paper job sheets can be captured with AI document scanning into Access.
Billing and Accounting
Agreement invoices, renewals and repair work usually end up in QuickBooks. Syncing customers and invoices directly avoids retyping; see Access QuickBooks integration. For fleet and van maintenance alongside customer equipment, see the fleet maintenance log.
FAQ
HVAC maintenance agreement tracking FAQ
Visits, renewals and replacement leads