Data import
What it's for
Data import brings your existing customers, contacts, sites, equipment, service agreements, and service history in from a file. It reads your columns, proposes what each one maps to, shows you what it is about to create, and only then writes anything.
Open jobs and open invoices are two more sheet kinds. Open invoices come in as a starting balance; a customer's outstanding balance imports as one opening-balance invoice. ServiceTitan open invoices read the Accounts Receivable (Detail) report (Invoice, Date, Customer, Total, Balance); Housecall Pro open invoices read the exported invoice report (Amount is the total, Due is the balance); Jobber open invoices read the Invoices Report (with the optional Total ($) and Balance ($) columns) and Jobber customer balances read the Client Balance Summary Report. Open jobs for all three, and both kinds for FieldEdge and BuildOps, use manual column mapping: open jobs need a job ID, a customer ID, and a status (site, date, and description are optional); open invoices need a customer ID, an invoice number, a date, a total, and a balance. Zero balances are skipped and credit balances go to review. Your accounting system stays the system of record for QuickBooks: payments taken in Thermal on an imported invoice are not sent to QuickBooks until your company turns pushing on, so record them there too until then.
Connect a source marks ServiceTitan, Housecall Pro, Jobber (client, property and job exports), and QuickBooks as Export upload. Export a file from that system and upload it below — Thermal does not log in to those products. Jobber has no equipment records, so each client's completed jobs land as service history on one unassigned unit at the property the job was done at, matched by the job's service address; move them to the real unit once it is recorded. A job whose address matches none of the client's properties lands on the client's first property and is listed on the review. A job with no completed date is skipped and listed on the review. Direct API connections for ServiceTitan, Housecall Pro, QuickBooks, and Workiz are not available. Workiz still starts from a CSV you export yourself.
Reach it from Data and connections → Data import, at /import.
Tabs
Before a file is chosen the screen offers two tabs: New import and Import history. Once a file is loaded, the import itself is a sequence rather than a set of tabs: upload, map, review, commit.
Panels and controls
Upload
Download template gives you the shape of a clean file if you are building one from scratch. Otherwise upload the export you already have — the importer is built to read files it did not design.
Mapping
Each of your columns is shown with a Source column, a Sample value, what it Maps to Thermal field, and a Status. Change overrides a mapping the importer got wrong. Seeing a real value from your own file next to the field it will land in is what makes a mapping mistake obvious before it is permanent.
Review
Before anything is written you see the records that will be created — Customer, Contact, Service address, Equipment, Agreement, Service history, Open job, and Open invoice — each with a Confidence, plus any Issue and the Action proposed for it. Ticking Import on a row includes it; leaving it unticked leaves it out. A row the importer is unsure about is surfaced rather than guessed at.
Commit
Only the rows you selected are written. Go to Customers takes you to the result.
Try again re-runs a step that failed rather than making you start over.
Import history
Lists every committed import pass, newest first: when it ran, the source file, who ran it, and how many customers it created that are still on file. Each row offers Delete this import, which soft-deletes the customers that pass created.
The delete is all-or-nothing and refuses rather than partially applying: if any customer from the run now has a service agreement, job, or unpaid invoice, the run is refused and every blocking customer and reason is listed, with no writes. Customers the run merged into rather than created are never touched. A successful delete stamps the run as reverted and writes one audit entry (who, when, run, row count).
What it accepts
| Field | Format | Required | Rule |
|---|---|---|---|
| Import files | .csv, .tsv, .iif, .qbo, .ofx, or .xlsx | Yes | Up to 5 files per pass, totalling up to 25 MB |
| Source lines | A mapped customer list is read in full; files that need automated structuring are read in requests of up to 150 lines, at most 8 per pass | Yes | If lines are not read, split the export into smaller files and repeat the header row in each file |
| Column mapping | Pick a Thermal field per column | Yes, per mapped column | Unmapped columns are ignored rather than guessed |
| Row selection | Tick the rows to import | Yes | "No customer rows selected for import." |
Spreadsheets must be the modern .xlsx format — the older .xls binary format is not supported; save it as .xlsx or CSV and upload again.
If the review screen reports that source lines were not read, import the split files one pass at a time. The 25 MB total applies separately to the files selected for each pass.
Import has to be switched on for your company; without it the screen says "Import not available — contact support to activate."
Customers by branch
A company with more than one branch can say which branch serves each customer. Map a Branch column on the Customers sheet to Customer · Branch and put the branch's name, as it appears in Settings, in each row (capitalisation does not matter). The customer, its sites, its equipment and its agreement go to that branch. A row that names a branch the company does not have is listed in the review and left out; the other rows still import. Someone whose access is limited to one branch can import rows only into that branch. A blank cell, or no Branch column, keeps the customer in your own branch, or in the company's default branch. A customer already in Thermal stays in its branch.
Equipment by site
An Equipment sheet can say which of a customer's sites each unit sits at. Map that column to Equipment · Site and put the site's name (the label column of a Sites sheet, or a site already on the customer in Thermal) or its street address in each row. The unit lands on that site. A row that names a site the customer does not have, or one that matches more than one site, is listed in the review and left out at import with the reason; the other rows still import. Without the column, every unit lands on the customer's first site.
Agreements by agreement id
An Agreements sheet with an agreementExternalId column imports one agreement per row, so a customer can have several, and importing the same file again updates those agreements instead of adding new ones. The sheet can be uploaded on its own: each row's customer is matched to a customer already in Thermal, or to one on a Customers sheet in the same upload.
| Column | Format | Rule |
|---|---|---|
| agreementExternalId | Text | Required. Unique across your agreements; a repeated id in one upload refuses every row carrying it |
| customerExternalId | Text | The customer's external id; without one, the customer's name must match exactly one customer you can see |
| siteLabel | Text | The site's name on that customer, as it appears in the label column of a Sites sheet in the same upload or on the customer already in Thermal; blank means the agreement covers the whole account |
| billingFrequency | monthly, quarterly, annual, or blank | Anything else refuses the row |
| escalationKind | fixed, index, or blank | A fixed escalation needs escalationBps; an index escalation leaves it blank |
| escalationBps | Whole number from 1 to 10000 | 300 means 3% per renewal |
| includedLaborHours | Hours, up to two decimals | Stored as minutes |
| ntePerCall | Dollar amount | Not-to-exceed amount for one call |
The other Agreements columns (status, plan name, dates, annual price, cadence, auto-renew, notes) are read as before. A column the sheet does not have leaves the stored value alone; a blank cell clears it, except status, cadence and auto-renew, which keep their stored value.
The import template has a Sites sheet (customerExternalId, label, address columns). Map its label column to Site · Name and each building keeps its name. The review lists every agreement row that names a site neither the upload nor Thermal has, before you import.
A row is refused, and nothing is written for it, when its customer or site is unknown, matches more than one record, or is outside your branch; when its id belongs to a deleted agreement or to another customer; or when a value breaks the rules above. The review step lists refused rows before anything is written. After the import, the result shows created, updated, unchanged and refused counts for each sheet, and Download agreement row errors gives every refused row with its file, sheet, row number and reason.
Covered units by agreement id
An Agreement units sheet (also named agreement_units, Agreement
equipment or Covered units) adds units you already have to the
equipment an agreement covers. It adds coverage only: it never creates a unit
or an agreement, so import those first or in the same upload. It can be
uploaded on its own.
| Column | Format | Rule |
|---|---|---|
| agreementExternalId | Text | Required. The agreementExternalId of the agreement, as imported by agreement id |
| equipmentExternalId | Text | The unit's external id. When given, it must match exactly one unit |
| serial | Text | The unit's serial number; spaces, hyphens and case are ignored when matching |
Each row needs the agreement id and at least one of the unit's external id and serial. A row is matched like this:
- An external id that matches no unit refuses the row. The serial is never tried as a fallback.
- A serial on its own must match exactly one unit that is at the agreement's customer, and at its site when the agreement has one.
- With both, they must name the same unit.
- The unit must belong to the agreement's customer, and to the agreement's site when it has one.
A row is also refused when the agreement or the unit is unknown, deleted, in another company or outside your branch; when a contractor rental's non-compete pledge blocks the agreement from covering the unit; or when the person committing the import does not have the Agreements area as well as Customers and Settings. The check is made when the import is reviewed and again when it is committed. A committer without a staff record cannot add coverage.
A new unit is priced and given its response window exactly as when you add it from the agreement page: against the agreement's rate card, counting the units already covered plus this one. A unit that is already covered is left exactly as it is, with its price and response window, so importing the same file again changes nothing and adds no new history. A refused row writes nothing and does not stop the other rows. The result lists created, unchanged and refused counts for the sheet, and every refused row with its file, sheet, row number and reason.
Visit schedules by agreement and visit type
A Visit schedules sheet, or a file named visit_schedules.csv, imports the
recurring visits of agreements that already exist in Thermal. It can be
uploaded on its own. In a workbook that also has an Agreements sheet, the
agreements are imported first, so a schedule can name an agreement created in
the same upload. The import template has a Visit schedules sheet with these six
columns.
- agreementExternalId (required, text): the agreementExternalId of one of your agreements. A blank, unknown, deleted or other-branch id refuses the row.
- visitType (required, text up to 120 characters): with the agreement it names the schedule. Spaces around it are ignored; capitals matter.
- frequencyMonths (whole number from 1 to 120): months between visits. Blank counts as 1 when valid frequencyDays is given; a row with neither is refused.
- frequencyDays (whole number from 1 to 3660, or blank): days between visits. When given, it replaces the month cadence.
- nextDueOn (YYYY-MM-DD): required for a new schedule. A date that does not exist, such as 2026-02-30, refuses the row.
- routeId (text up to 80 characters, or blank): the territory label for the schedule, kept as typed.
Matching. One agreement and one visit type are one schedule. Importing the same pair again updates that schedule in place and keeps its id, so work orders already linked to it stay linked. A new visit type adds a schedule, and a changed visit type is a new schedule, not a rename. Schedules the sheet does not list are left alone, including inactive ones. Inactive schedules are matched like any other and stay inactive. A new schedule starts active. An agreement that came from a plan template keeps that link, and the import adds only the rows you list, never the template's presets.
Missing and blank. A column the sheet does not have leaves the stored value alone. A blank frequencyDays or routeId cell clears the stored value. A blank frequencyMonths cell beside valid days keeps the stored months. With no frequencyMonths column at all, a blank frequencyDays cell clears the days and the stored months stay.
Dates that would move backwards. Once a schedule has generated visits, or has any work order linked to it, an imported nextDueOn earlier than its current due date refuses the row and changes nothing on it. The same date or a later one is accepted. A schedule that has never generated a visit can move earlier. The current date is read after booking releases the schedule, so a visit booked while the import runs is respected.
Row errors. A row is refused, and nothing is written for it, when any of these is true:
- the agreement id is blank, unknown, deleted or outside your branch
- the same agreement and visit type appear more than once anywhere in the upload, in which case every copy is refused, even a copy that also has a bad value
- more than one existing schedule has that agreement and visit type; fix the duplicates in Thermal first
- a value breaks the rules above
- the date would move backwards, as described above
The review step lists value errors before anything is written, and the result shows created, updated, unchanged and refused counts for each sheet. Download agreement row errors gives every refused row with its file, sheet, row number and reason.
How to configure it
- Import availability is set for your company rather than by you — the message above tells you when it is not on.
- Branches decide which branch imported records are assigned to.
- Price book tiers decide the pricing imported customers land on.
- Equipment types come from the trades your company has enabled, so importing equipment for a trade you have not turned on will not map cleanly. A unit whose type your company has not set up is not imported and is never dropped silently: the review counts it as excluded, lists it by row with the type, and links to Settings > Equipment types. Add the type there and upload the file again to bring those units in.
Who can see it
This screen requires both the Customers area and the Settings area.
Owners and office managers can open it when neither area has been removed. Dispatchers and technicians cannot. A custom role that removes Customers or Settings cannot open it. The gate is checked on the server, so a direct link does not get around it.
The full role matrix is on Roles & permissions.