> ## Documentation Index
> Fetch the complete documentation index at: https://docs.voxworks.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Importing Custom Variables from CSV

> Create typed custom variable records in bulk from a spreadsheet — the custom.{type}.{field} header convention, column mapping, type conversion rules, and where typed CSV import appears across the app.

## Overview

Typed CSV import lets you load per-contact data in bulk: one spreadsheet creates (or updates) your contacts **and** a typed custom variable record for each of them, linked and ready for calls to read. If you're importing a call list where every row carries extra columns — a budget, a policy number, an appointment preference — this is how those columns become `{{custom.<type identifier>.<field>}}` values your scripts can speak.

The import is driven by a **custom variable type**: you pick the type, map your CSV columns onto its fields, and every value is checked against the field's data type before anything is written. If you haven't built a type yet, start with [Custom Variable Types & Schema Validation](/custom-variables/types).

***

## Where you can import

Typed CSV import appears in every place you upload contacts:

| Entry point               | How to reach it                                                                                  | What it does                                                                                                                   |
| ------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| Contacts page             | The **Upload CSV** button in the toolbar opens the **Upload Contacts** dialog                    | Creates new contacts and offers to update existing ones matched by phone number                                                |
| Lists                     | **Add Contacts from CSV** when adding contacts to a list                                         | Creates the contacts and adds them to the list                                                                                 |
| Batch Scheduler           | Drag and drop a CSV onto the source lists area of the scheduler — opens **Create list from CSV** | Creates a named list from the file (the list name is pre-filled from the file name) and adds it as a source list for the batch |
| Add Calls to Batch dialog | The **Upload CSV** tab                                                                           | Imports the contacts and adds them straight to the batch queue                                                                 |

Every entry point shares the same flow: choose or drop a `.csv` file (Excel files are not supported), review the parsed rows, optionally map custom variable columns, then import. The Contacts, Lists and Add Calls to Batch dialogs also have a **Download CSV Template** button with sample contact rows; the Batch Scheduler's drag-and-drop area skips it, but its review step still offers the **Typed template** download once you choose a custom variable type.

The Custom Variables page itself offers CSV **download** of the current table view, but no upload — typed records are imported through the contact flows above, or created individually with **Add Custom Variable**.

The Contacts page CSV download round-trips with this import: alongside the contact columns, it writes one `custom.<type identifier>.<field>` column for every field of every custom variable type, filled from each contact's linked records. You can export your contacts, edit the values in a spreadsheet, and re-import the same file — the headers map automatically.

***

## Contact columns

The importer recognises these contact columns by header (alternate spellings such as `first_name` or `phone` also match):

| Column                               | Required | Notes                                                                                                                                                                      |
| ------------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| First Name                           | Yes      | If blank, it's derived by splitting **Full Name**                                                                                                                          |
| Last Name                            | No       | `Surname` also matches                                                                                                                                                     |
| Phone Number                         | Yes      | Must be an international-format number, e.g. `+61412345678`. Common Australian formats — `0423 456 789`, `(08) 9123 4567`, 1300/1800 numbers — are converted automatically |
| Full Name                            | No       | Used only when both First Name and Last Name are empty, split on the first space into First/Last                                                                           |
| Email                                | No       | Must be a valid email address when present                                                                                                                                 |
| Address                              | No       | Up to 500 characters                                                                                                                                                       |
| Company                              | No       | Recognised in the Contacts importer and the Add Calls to Batch dialog's Upload CSV tab; not read by the Lists or Batch Scheduler importers                                 |
| Status, Age, Gender, Location, Title | No       | Age must be 0–150; Gender must be `male`, `female` or `other`                                                                                                              |

Rows that fail validation are listed in the review step with the reason and are excluded from the import. Rows repeating a phone number already seen in the file are skipped. In the Contacts importer, rows matching an **existing** contact's phone number appear in a separate "Existing contacts to update" section — tick them to update those contacts instead of creating duplicates.

***

## The `custom.{type}.{field}` header convention

Any column that isn't a recognised contact column is treated as a candidate custom variable column. To have columns map themselves automatically, name them with the stable key convention:

```
custom.<type identifier>.<field>
```

The `<type identifier>` is the type's internal identifier (e.g. `property_listing`), not its display name — see [Custom Variable Types & Schema Validation](/custom-variables/types) for how that identifier is generated from the type's display name.

For example, for a Property Listing type:

```csv theme={null}
First Name,Last Name,Phone Number,custom.property_listing.budget,custom.property_listing.inspection_date
Jane,Citizen,+61412345678,750000,2026-07-21
```

Nested fields use their full dotted path, e.g. `custom.property_listing.address.street`.

You don't have to build the headers by hand. In the import dialog's **Per-contact custom variables** panel, choose your type and select **Typed template** — it downloads a CSV with the core contact columns plus one correctly named column per field of the type, pre-filled with an example value matching each field's data type.

Auto-matching is forgiving: after stripping the `custom.` prefix, a header matches a field if it equals the field's key, path or display label, ignoring case, spaces and punctuation. So `Budget`, `budget` and `custom.property_listing.budget` all map to the same field. Anything the importer can't match you can map by hand.

***

## Mapping columns to fields

In the review step, the **Per-contact custom variables** panel controls the typed import:

1. **Choose a type** — pick a custom variable type from the dropdown, or leave it on **Do not import custom variables** to import contacts only.
2. **Map each column** — every unrecognised CSV header appears with a dropdown of the type's fields. Each option shows the field's name and data type, and marks required fields; the batch import also shows the expected value format under each mapped column. Set any column you don't want to **Ignore column**.
3. **Fix errors** — every **required** field of the type must be mapped to a column, and every row must supply a value for it. Problems are listed per row, e.g. `Row 2 (+61412345678): Budget: must be a number`, and the import is blocked until they're resolved: "Fix the custom variable errors before importing". The row number counts the header as row 1 and follows the rows you're importing — if earlier rows were dropped for failing contact validation or as duplicates, later row numbers no longer match their original line in the file.

***

## Type conversion rules

Each mapped cell is converted according to the field's data type. An empty cell leaves the field unset (unless the field is required, which is an error).

| Field type                       | Accepted CSV values                                               | Errors                                                                                                                 |                                                                                                                                     |
| -------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Text                             | Any text, taken as-is                                             | —                                                                                                                      |                                                                                                                                     |
| Text with **Allowed values** set | One of the field's allowed values, matched ignoring case          | "must be one of: …"                                                                                                    |                                                                                                                                     |
| Number                           | Digits, with `$` and `,` stripped (so `$1,500` imports as `1500`) | "must be a number"; if the field has a Minimum or Maximum set, values outside that range are also rejected             |                                                                                                                                     |
| Yes/No                           | `yes`/`no`, `y`/`n`, `true`/`false`, `1`/`0` (any case)           | "must be yes/no, true/false, or 1/0"                                                                                   |                                                                                                                                     |
| Date                             | `YYYY-MM-DD`, e.g. `2026-07-21`                                   | "must use YYYY-MM-DD"                                                                                                  |                                                                                                                                     |
| Date & Time                      | Any parseable date-time, e.g. `2026-07-10T09:30:00+10:00`         | "must be a valid date and time"                                                                                        |                                                                                                                                     |
| List                             | Pipe-separated items (\`item one                                  | item two`), or a JSON array (`\["a","b"]\`)                                                                            | "must be a JSON array" if the value starts with `[` but isn't a JSON array; a malformed `[` value shows a raw parsing error instead |
| Object                           | A JSON object, e.g. `{"street": "12 Wattle St"}`                  | "must be a JSON object" if the value parses but isn't a plain object; malformed JSON shows a raw parsing error instead |                                                                                                                                     |

***

## What happens when you import

For every selected contact, the import creates **one typed record of its own** and links it to that contact — contacts do not share a single record. The record is named after the contact (falling back to the phone number when no name is given). If a contact already has a linked record of the chosen type, that record is **updated** with the new values instead of a duplicate being created — so re-importing a refreshed spreadsheet is safe.

Before writing, every record is validated against the type's schema:

* If the type's validation mode is **Strict**, a record that fails validation stops the import with an error naming the contact and the problem.
* Otherwise, mismatches are recorded as warnings and the data is saved anyway. See [Custom Variable Types & Schema Validation](/custom-variables/types) for how the validation modes behave.

The batch flow confirms the result with a count of custom values created and updated. In the Contacts importer, if the contacts save but their typed values can't be, you'll see "Contacts imported, but their custom variables could not be saved." — the contacts are kept, and you can re-run the import to attach the values.

Once imported, the records appear on the Custom Variables page under their type's tab, and each contact's linked record feeds calls with that contact: your script reads `{{custom.property_listing.budget}}` (Number, e.g. `750000`) or `{{custom.property_listing.inspection_date}}` (Text, e.g. `2026-07-21`) from the record linked to whoever is on the call. How linked records are chosen at call time is covered on [Linking Custom Variables to Contacts](/custom-variables/linking-contacts).

***

## Next Steps

* [Custom Variable Types & Schema Validation](/custom-variables/types) — build the type schema your CSV columns map onto, and choose a validation mode.
* [Linking Custom Variables to Contacts](/custom-variables/linking-contacts) — how linked records are read on calls, and other ways to link them.
* [Importing & Exporting Contacts](/contacts/import-export) — the full contact CSV workflow, including duplicate handling and phone formats.
* [Adding Contacts to a Batch](/batches/adding-contacts) — build a calling queue from lists, contacts or a CSV drop.
