Import & Export
What Settings → Import / Export actually does for each entity, exactly how row validation and duplicate handling work, and the full mechanics of the bulk Purchase Order importer.
On this page
Where it lives
Both products have their own Import / Export tab, because each imports a different set of entities: Equipt Settings → Import / Export and Landscapt Settings → Import / Export. Each tab is self-contained — an Export Data panel of entity tiles at the top, an Import Data panel of entity tiles below it. There is no cross-product import screen; Vendors (shared between Equipt and Landscapt) are imported from the Equipt tab only.
On an org without Equipt enabled, the Equipt-only tiles (Work Orders, Assets, Vehicles, Parts) are hidden from both the export and import panels — only Vendors, Requisitions, Products, and Purchase Orders remain, since those are shared with the PO backbone.
Exporting data
Export is real, and it is genuinely a full data export, not a template stub — each tile downloads a CSV of every current record for that entity (not just headers), in the same column order and naming as that entity's import template. Clicking a tile with no records in it does nothing (the export handler returns early rather than downloading an empty file).
Money fields are converted from stored cents to a formatted dollar string on the way out (e.g. Parts' unit cost, Purchase Orders' grand total). Everything else is written as-is from the record.
Safe to open in Excel or Sheets. A text value that starts with =, +, -, @ or a tab would be run as a formula by a spreadsheet, so the export puts a single quote in front of it (a client name typed into a public form as =HYPERLINK(...) shows as plain text instead of executing). Plain numbers, including negative amounts, are left alone. When you import a file, that protective leading quote is removed again (only in front of =, +, - or @), so an export round-trips cleanly.
ImportExportMenu, used on a few standalone list pages like Parts and Products) that isn't wired into the Settings Import/Export tab. In practice, an export doubles as the template: its headers match exactly what the matching import tile expects, so exporting an entity with a few existing rows and clearing the data is the fastest way to see the exact columns before a bulk import.How an import actually runs
- Click an entity tile in the Import Data panel — this opens a file picker restricted to
.csv. - The app reads your CSV's header row and tries to auto-map every expected column against it: exact match first, then a case/punctuation-insensitive match, then a table of common aliases (e.g. a column literally titled "Part #", "SKU", or "Part No" all auto-map to
partNumber; "Qty" or "On Hand" map toquantityOnHand). - If every expected column matched automatically, it skips straight to the preview. If anything didn't match, a Map Columns dialog opens listing every expected field with a dropdown of your CSV's actual columns (or "Skip") — this dialog is effectively the field reference, since there's no separate template download.
- Fields marked with a red asterisk are required for that entity. You cannot continue past the mapping dialog until every required field has a column assigned.
- A preview step shows the first 5 mapped rows in a table and the total row count, plus an Import Error state if the file was empty, unreadable, or missing a required mapping. This is the only pre-commit check — there is no server-side dry run separate from this client-side preview.
- Clicking Import N Rows sends every mapped row to the entity's import handler at once. A success or error banner appears above the Export/Import panels when it finishes.
What you can import, by entity
Equipt (Settings → Equipt → Import / Export):
| Entity | Required columns | Other columns the template accepts |
|---|---|---|
| Work Orders | title | workOrderNumber, description, priority, status, category, assetName, assignedToName, dueDate, createdAt |
| Assets | name, assetTag | equipmentNumber, assetType, make, model, year, serialNumber, location, status, purchaseVendorName, purchaseDate, purchasePrice, paymentMethod, financeInstitution |
| Vehicles | name, assetTag | make, model, year, licensePlate, vin, fuelType, status, assignedCrew, purchaseVendorName, purchaseDate, purchasePrice, paymentMethod, financeInstitution |
| Parts | name, partNumber | description, category, unitCost, quantityOnHand, minimumStock, vendorName, location |
| Vendors | name | contactName, email, phone, address (street), city, state, zip, vendorType, website, notes |
| Requisitions | title | vendorName, notes |
| Products | name, partNumber, category | description, unitCost, price, quantityOnHand, vendorName, isInventory |
| Purchase Orders | none enforced — see dedicated section below | Purchase Order #, Vendor, Status, Created On, Approved On, Completed On, Due Date, Line Type, Line Name, Part Number, Unit Cost, Ordered Quantity, Ordered Cost |
Landscapt (Settings → Landscapt → Import / Export):
| Entity | Required columns | Other columns the template accepts |
|---|---|---|
| Clients | displayName | firstName, lastName, accountType, status (active, lead, inactive, cancelled, lost; anything else imports as active with a warning), clientSince, primaryPhone, primaryEmail, billingEmail, billingAddress/City/State/Zip, serviceAddress/City/State/Zip, source, accountNumber, parentAccountNumber (links the row as a sub-account of that account), tags (separate with |), notesToCrew |
| Leads | displayName | accountType, primaryPhone, primaryEmail, billingAddress/City/State/Zip, source |
| Estimates | clientName, description | estimateDate, validUntilDate, poNumber, stage, amount, salesperson, probability, externalRef |
| Invoices | clientName, service, description, amount | invoiceDate, dueDate, poNumber, status, taxAmount |
| Contracts | clientName, title | status, startDate, endDate, monthlyAmount, billingFrequency, billingDayOfMonth, billMonthInAdvance, paymentType, poNumber, autoGenerate, isActive, includeSubProperties, defaultService, invoiceLineItems (separate lines with |), janAmount … decAmount (optional seasonal amounts) |
| Payments | clientName, amount | paymentDate, method, reference, memo, invoiceNumber |
| Tickets | subject | clientName, type, status, priority, category, body, dueDate |
| Services | name | code, category, unit, defaultRate, productionRate, isActive. Rates may include $ and commas ("$1,200.50"). Units accept common wording ("Sq Ft", "hour", "each") and anything unrecognised falls back to "visit". A row with a production rate sets the service to budget by production rate; a blank production rate on a re-import leaves the saved one alone. |
| Schedules | name, frequency (weekly, bi_weekly, every_3_weeks, every_4_weeks, monthly, custom) | dayOfWeek (required except for custom), weekPattern, anchorDate, seasonStart, seasonEnd, weekOfMonth, customDates (required for custom), excludedDates — date lists are separated with | , e.g. 2026-07-06 | 2026-07-20 |
| Employees | firstName, lastName | email, phone, cellPhone, address, city, state, zip, dateHired, resourceCode, hourlyRate |
partNumber renders as "Part Number"). Your CSV's actual header text almost never needs to match exactly — the alias matching described above handles the common variants.Worked example: importing a batch of vendors
Say you're bringing over 40 vendors from a spreadsheet with columns Supplier, Contact, Phone, Email, and Type.
- Settings → Equipt → Import / Export → click the Vendors tile under Import Data.
- Upload the file.
nameis the only required field for Vendors, and "Supplier" doesn't auto-match it (it's not in the alias list), so the Map Columns dialog opens with Name still unmapped. - Map Name → Supplier, Contact Name → Contact, Email → Email, Phone → Phone, Vendor Type → Type. Address, Website, and Notes have no matching column in your sheet, so leave them as "Skip" — they aren't required.
- Continue to Preview — confirm the first 5 rows look right, then click Import 40 Rows.
- Behind the scenes, rows import one at a time. Any row missing a name is silently dropped from the count entirely (not created, not reported as an error). For every remaining row, a vendor with that exact
namealready on file is updated in place with the new row's contact info, address, type, website, notes, and active flag — it does not create a duplicate vendor. Everything else inserts as a new vendor.
The same shape applies to Parts (keyed on partNumber instead of name) and most other entities — see the table in the next section for what's keyed on what.
Validation, duplicates, and what gets skipped
The FAQ's short version — "the importer validates each row before committing" — is really two separate mechanisms:
- Required-field validation happens twice: once at the column-mapping step (you can't proceed if a required field has no column assigned), and again per-row at import time (a row whose required value is blank after trimming whitespace is dropped and counted as "skipped" — it never reaches the database, and it does not stop the rest of the batch).
- Duplicate handling is not a validation failure — it's a merge. Rows are inserted one at a time; when the database rejects an insert because of a unique constraint (Postgres error code
23505), the importer treats that as "this record already exists" and updates the existing row instead, rather than erroring or creating a second copy. - For entities not listed as reporting row by row, any other database error (not a duplicate-key violation) is not swallowed — it throws, which stops the mutation and surfaces the error message in the preview dialog's error banner. Rows already committed earlier in that same batch stay committed; rows after the failure are never attempted. There is no all-or-nothing transaction wrapping the whole CSV.
- File format. The importer reads UTF-8, Windows-1252 (what Excel's “CSV (Comma delimited)” saves on Windows) and UTF-16 (“Unicode Text”) files, so accented letters and curly quotes come through intact. It detects whether columns are separated by commas, semicolons or tabs from the header row. Money cells may include
$and thousands separators ($1,250.50). - Results summary. When some rows import and others fail, the dialog says so (for example “12 imported, 3 failed”) and lists the failing rows with the reason, rather than reporting success. Rows deliberately not imported because they match an existing record are listed separately as skipped (duplicates), not as failures.
- Vendors, Assets and Vehicles also report row by row: a row with a missing name or tag, an unreadable date or price, or a database error is listed as failed with the reason, and the rest of the file still imports.
- Estimates, Invoices and Payments are more forgiving about bad rows: one bad row is reported and skipped instead of stopping the import. Amounts can be typed the way Excel or QuickBooks writes them (
$1,234.50), and dates can beYYYY-MM-DDorM/D/YYYY. A row is skipped, with the reason shown, if its amount or date can't be read, if no client matches, or if two clients share the name on the row (add anaccountNumbercolumn to say which one). Re-importing the same file creates the records again, so import each file once. - Estimate import extras.
amountadds one editable line, “Imported estimate total”, so the estimate carries its old total instead of $0.salespersonis matched to an employee by full name (a blank, unknown or duplicated name just leaves the rep empty).probabilitytakes a percent such as60or60%.externalRefis the estimate number in the old system; it is saved in the estimate's notes as “original estimate #”.
| Entity | Matched on | On match |
|---|---|---|
| Vendors | name (trimmed, not case-sensitive, org-scoped; a name repeated inside one file counts as the same vendor) | Existing vendor is updated with only the columns the row fills in (contact, email, phone, address, city, state, zip, type, website, notes). An older file with the whole address in the address column still imports; it lands in the street field, so put the city, state and zip in their own columns. A blank cell never erases saved details, and the Active column only changes the vendor's status when the cell has a value (Active, Inactive, true, false, yes or no). Renaming a vendor in the app also renames it on its parts, products, assets, vehicles and other records that show the vendor's name. |
| Parts | partNumber (unique per org) | Existing part is updated with only the columns the CSV carries (name always; description, category, unit cost, minimum stock, vendor, location, inventory flag when present). A quantity-only or cost-only file leaves everything else alone. A quantity on hand in the CSV is applied as a recorded stock adjustment (reason “CSV import”), not a silent overwrite, and only when it differs from the current count; a blank quantity cell leaves stock as it is. The part’s catalog product is kept in step. |
| Products | partNumber (within the org); rows with no part number are matched on exact name | A row with no part number whose name already exists (ignoring case) is skipped as a duplicate. Existing product is updated with only the columns the CSV carries. A blank quantity, price, unit cost or vendor cell leaves the saved value alone, so a cost-only file no longer zeroes stock or resets prices. Stock changes go through a recorded adjustment (reason “CSV import”) so cost layers and the audit trail follow. Quantities accept decimals for materials; maintenance parts must be whole numbers. |
| Assets | assetTag | Existing asset is updated with only the cells the CSV filled in (make, model, year, serial, location, purchase details, asset type when a type is given). Blank cells never wipe saved values and a blank status keeps the current status. |
| Vehicles | assetTag (looked up first, so a repeat import never creates a second copy) | Existing vehicle is updated with only the cells the CSV filled in. A blank status keeps the current one, and a VIN that belongs to another vehicle is reported as a failed row. |
| Work Orders | workOrderNumber | Existing work order is updated with only the columns the CSV carries (title always). A completed work order is not reopened by a file that has no status column, and blank descriptions, assets or due dates don’t erase saved ones. |
| Clients | accountNumber when the row supplies one; otherwise primaryEmail or primaryPhone; a row with none of those is matched on name plus service street | A matching account number updates the existing client with the row's contact and address fields. A row whose account number is blank or new but whose email or phone matches an existing client is skipped as a duplicate and listed in the skipped rows. Everything else inserts as a new client; a row without an account number gets the next one from Settings (prefix, next number, suffix), and numbers your file already gave to other clients are skipped, so imported numbers never block later new clients. |
| Leads | primaryEmail or primaryPhone against every existing non-deleted client | No new record is created. Instead the matched client is tagged “imported-lead” and the row is counted separately as “matched,” not “created.” |
| Services | code when the row has one; otherwise the row is added as a new service | A matched service is updated with only the columns the file fills in, so a file without category, unit, rate or active columns leaves those alone. Rows without a code create a new service every import, so give services a code if you plan to re-import. A row that fails is listed with its line number; the rest still import. |
| Employees | email; rows with no email are matched on full name | A matched employee is updated with only the cells your file filled in: blank phone, address or hourly rate cells never erase saved values. An employee with no email whose first and last name already exist is skipped as a duplicate. Hourly rate accepts $ and thousands separators. |
| Contracts | client + contract name + start date | The row is skipped with a note — an existing contract is never overwritten. Clients are matched by exact name; a name shared by two clients, or not found, skips the row. Status defaults to draft, so set it to active in the file for contracts that should bill. Imports don’t fire automations. |
| Purchase Orders | po_number (derived as PO-{year}-{csv PO #}) | The PO is skipped and listed in the failed-rows summary (“already exists”) — not merged, not counted as imported. Existing POs are never overwritten by a re-import. |
Purchase Order import, in detail
Purchase Orders import differently from every other entity — it's the one importer built for a denormalized export format (multiple CSV rows per PO: one row per line item, repeating the PO-level fields on every row) rather than one row per record. No column is marked required in the UI, because the importer groups rows by Purchase Order # itself — a row with no PO number is silently dropped from the group before anything is grouped.
- All rows sharing the same
Purchase Order #are grouped together; the first row in the group supplies the PO-level fields (Vendor, Status, Created On, Approved On, Completed On, Due Date). - Vendor is matched by name (case-insensitive) against existing vendors; if no match exists, a new vendor is created on the fly from just that name.
- Rows are split by
Line Type:PARTlines become PO line items and drive the subtotal;PERCENT_TAXABLElines set the tax rate (parsed out of the line name, e.g. "CT Sales Tax (6.35%)") and tax amount;AMOUNT_TAXABLElines are summed into shipping cost. The PO'spo_numberis generated asPO-{year}-{csv PO #}, where the year comes from the CSV's Created On date, not today's date. - If a PO with that exact generated number already exists, the whole PO (and all its lines) is skipped — not merged, not updated, not counted as imported. Re-running the same export is safe; it won't duplicate POs.
- For each PART line, the importer resolves a Products catalog entry by
Part Numberfirst, falling back to matching by item name only when no part number is given. If nothing matches either way, a new catalog entry is created (category defaults tomaintenance_part). A line with neither a usable part number nor a name is skipped rather than inserted without a catalog reference — every PO line item must reference a Products catalog entry per this platform's data rules. - Quantities accept decimals (for example 2.5 gallons) and a blank cell means 1. Maintenance parts must be whole numbers, and text or zero/negative quantities are rejected. Every row that can't be imported — a duplicate PO, a bad quantity, a line with no catalog match, or a line the database refused — is listed with its row number and reason in the results summary instead of being dropped silently.
- When the resolved catalog entry has a part number, the importer also syncs it into CMMS Parts inventory: it links to an existing Part with that
product_item_idorpart_numberif one exists, or creates a new Part record at zero quantity on hand otherwise. Quantity on hand is not incremented by a PO import — that only happens through the separate Goods Receipt flow.
The name-conflict audit trail, worked example
The Products catalog and Parts inventory each keep one name per part number — whichever name they saw first. If a later PO import uses a different name for a part number that's already on file, the importer does not overwrite the existing name and does not silently keep the newer one either — it leaves the catalog untouched and writes a name_conflict entry to that record's audit trail so the mismatch can be reviewed instead of lost.
OF-4521 labeled "Oil Filter" from Vendor A on PO-2024-1002. Six months later you import a second batch that includes the same part number, OF-4521, from Vendor B, but their line item is labeled "Premium Oil Filter" on PO-2024-1140. The catalog entry for OF-4521 stays "Oil Filter" — it is not renamed. Instead, an audit trail entry is written on that product (and on the linked Part, if one exists) reading something like: "Import name conflict on PO-2024-1140 (not applied): catalog has 'Oil Filter', import line used 'Premium Oil Filter'."This is intentionally conservative: if the two names really do describe the same interchangeable part, no action is needed — the catalog is still correct and the PO line still links to the right record. If they turn out to be two genuinely different parts that happen to share a part number by data-entry mistake, the audit trail entry is what tells you to go split them apart rather than assuming a later import quietly relabeled something.
The same check runs independently for the Products catalog entry and, when one is linked, the CMMS Part record — so a single conflicting import line can produce up to two audit trail entries: one on the product, one on the part.
Limitations
- CSV only. No Excel (.xlsx), JSON, or other format is accepted by either the file picker or the underlying parser.
- No standalone template download in this panel. Use an export of the same entity (even with a couple of dummy rows deleted afterward) as your column reference, or rely on the Map Columns dialog, which lists every expected field.
- No pre-commit server-side dry run. The 5-row preview table is a client-side rendering of your mapped CSV, not a validation pass against the database — duplicate and error handling only happens once you click Import.
- Re-importing updates only what the file contains. For Equipt entities (Products, Parts, Assets, Vehicles, Work Orders and Vendors) a matched record is updated with just the columns present and filled in; blank cells and missing columns leave the saved values alone, and stock quantities change only through a recorded adjustment. See the table above for each entity's matching key.
- Import only creates or merges — it never deletes. There is no way to remove records via CSV; re-importing an export you haven't edited is a safe no-op for entities that dedupe by a stable key, but it will not clean up records removed from the source system.
- Requisitions import has no vendor auto-create logic the way Purchase Orders does — check your vendor names are already in the system (or import Vendors first) if you're bulk-loading requisitions with a
vendorNamecolumn.
Good to know
- Client import round trip. The client export has the same columns the import accepts (including status, client since, names, billing email, tags, parent account and crew notes), so exporting and re-importing changes nothing and creates no duplicates.
- Row numbers. Failed, skipped and warning rows are listed with the line number in your CSV file (line 1 is the header). Rows that imported with something to check, such as an unknown status, are listed as warnings.
- New tags. Tags in a client file that aren't in Settings → Tags yet are added there when your role can edit CRM settings, so you can filter and bulk-tag by them.
- Leads import report. The leads import lists every row that failed or matched an existing client, with its line number.
- Quoted text with line breaks. A quoted cell that contains both doubled quotes (
""like this"") and a line break imports as one cell instead of splitting the row.