> ## 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.

# Linking Custom Variables to Contacts

> How to attach custom variable records to contacts — one by one, in bulk, by CSV, or through the API and automations — and what your voice agent reads on a call as a result.

## Overview

A custom variable record only becomes call data once it's connected to a contact. When your agent calls (or is called by) a contact, it reads the custom variable records **linked to that contact** and exposes their fields to your script through the `custom` namespace — for example `{{custom.property_listing.address}}` (string, e.g. `12 Wattle St, Brisbane QLD 4000`).

Linking is flexible in both directions:

* **One record, many contacts** — link the same Property Listing to every buyer you're calling about it.
* **One record per contact** — give each survey respondent their own answers record.

This page covers every way a link is made — the contact detail pane, the bulk toolbar on Contacts, CSV import, the API and automations — plus how the data is stored and exactly what the agent reads on a call.

***

## What the agent reads on a call

At the start of every call, the platform builds the contact's custom variable values in two passes:

1. **Type defaults** — custom variable types installed from the catalogue ship with a default record, and those defaults are loaded first, so `{{custom.<type name>.<field>}}` references resolve even for contacts with nothing linked. Types you build yourself carry no defaults — their fields read as empty until a record is linked.
2. **Linked records** — the records linked to the contact are laid over those defaults. A linked record's values win wherever they're set.

In any script field that supports variables, type `/` to insert these tokens — your installed types appear under the **custom** branch of the picker with each field's data type shown.

Keep **one record of each type** linked per contact. The workspace flows enforce this — the contact detail pane shows one slot per type, and bulk assignment replaces any existing link of the same type — but the API allows several. If multiple records of one type are linked, their values are combined into a single view in no guaranteed order, so overlapping fields become unpredictable.

Two authoring consequences:

* A field that's empty on the linked record *and* has no default reads as empty. In spoken text an empty variable is left as the literal token (and the call gets a system note flagging the unresolved reference), so guard optional fields before speaking them — see [Text Functions](/scripts/text-functions) for conditional text.
* Changes you make to a record between calls flow through automatically — the values are read fresh at the start of each call.

***

## Linking from a contact's details

Open **Contacts**, click a contact to open its details, and scroll to the **Objects** section — this is where the contact's linked custom variables live, with one row per custom variable type:

| State          | What you see                    | What you can do                              |
| -------------- | ------------------------------- | -------------------------------------------- |
| Nothing linked | A dashed **Select...** dropdown | Pick any record of that type to link it      |
| Record linked  | The record's name               | Click the **×** beside the name to unlink it |

The dropdown lists every record of that type that isn't already linked to the contact; if the type has no records yet, create one in the Custom Variables workspace first. Unlinking only removes the connection — the record itself stays in the Custom Variables workspace and any links to other contacts are untouched.

***

## Bulk assigning from the Contacts table

To link the **same record** to many contacts at once:

1. On **Contacts**, tick the checkboxes of the contacts you want.
2. In the toolbar that appears, click the **Assign Object** button (box icon).
3. In the popover, choose the **Object type**, then the **Value** — the record of that type to link.
4. Click **Apply**.

Bulk assignment is a *replace*, not an add: any existing link of the same type on the selected contacts is cleared first, so every selected contact ends up linked to exactly the record you chose. Links of other types are untouched.

This is the pattern for shared data — a promotion, an event, a listing — where every contact in a campaign should hear the same values.

***

## Seeing linked values on the Contacts table

The Contacts toolbar has a view toggle (box icon) whose tooltip reads **Show objects** — click it to switch the table into the custom variables view. The contact detail columns are replaced by one sortable column per custom variable type, showing the name of the record linked to each contact (or **-** when nothing is linked; multiple linked records show as a comma-separated list). Click the toggle again (**Show contact details**) to switch back.

Use this view to spot-check a list before launching a batch: an unlinked contact hears the type's default values (or nothing at all, if the type has no defaults), not record-specific ones.

***

## Creating one record per contact (CSV import)

When each contact needs their **own** values — survey answers, order details, account balances — importing a CSV is faster than creating and linking records one at a time. The contact importers (on Contacts, Lists and the Batch Scheduler) include a **Per-contact custom variables** step: pick a custom variable type, map your spreadsheet columns to its fields, and the import creates or updates one linked record per contact.

The update logic is an upsert, keyed on the contact:

* If the contact already has a record of that type linked, that record's data is **updated** in place.
* Otherwise a new record is **created** — named after the contact, falling back to their phone number — and linked.

Values are checked against the type's schema as they're imported; whether problems warn or block depends on the type's validation setting — see [Custom Variable Types & Schema Validation](/custom-variables/types). Up to 5,000 rows of custom variable data can be imported at once. The column header convention (`custom.<type name>.<field>`), typed templates and mapping rules are covered on [Importing Custom Variables from CSV](/custom-variables/csv-import).

***

## How the data is stored: linked records vs on-contact data

Every custom variable type has a storage behaviour, set by the `unique_by_contact` flag when the type is created through the API (types created in the app use the default, `false`):

| `unique_by_contact` | Where data lives                                                           | Behaviour                                                                                                                                     |
| ------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `false` (default)   | A record in the Custom Variables workspace, connected to contacts by links | One record can be linked to many contacts; visible and editable in the workspace; feeds `{{custom.<type name>.<field>}}` on calls             |
| `true`              | Merged directly onto each contact's own record                             | Exactly one copy per contact, merged field-by-field on every API write; no linking step and nothing appears in the Custom Variables workspace |

The values your scripts read through the `custom` namespace come from **linked records**. Data stored on the contact by a unique-per-contact type serves platform and integration workflows instead — for example, calendar integrations use it to remember a contact's booking identifiers between calls — and it's returned with the contact through the API.

There is no control for this flag in the app — set it when creating a type through the API. Speak to the Voxworks team if you need an existing type's storage behaviour changed.

***

## Linking through the API

The [Voxworks API](/api-reference/overview) covers the full round trip. In brief:

* **Attaching data when scheduling a call** — the create-phone-call endpoint accepts an `objects` payload: a map of type name to field values. Each entry is validated against the type's schema, then stored according to the type's `unique_by_contact` behaviour — merged onto the contact, or saved as a record and linked. If the data carries an `id` matching an existing record of that type, that record is updated rather than duplicated.
* **Upserting records** — the upsert endpoint matches an existing record by a key inside its data. Pass an `upsert_key` with a JSON path (for example `$.id`) and value; if the data itself contains an `id` field it's used as the default key. A match updates the record in place — keeping every existing contact link — otherwise a new record is created. This is what makes repeated syncs from an external system safe: the same order or listing never duplicates.
* **Linking and unlinking** — a dedicated endpoint links an existing record to an existing contact by their IDs. Records deleted through the API are hidden rather than destroyed — they drop out of the Custom Variables workspace and out of link pickers, but existing contact links are not automatically removed.

***

## Linking through automations

The **Upsert Custom Variable** pipeline step gives [Automations](/automations/overview) the same create-or-update-and-link ability, driven by trigger data:

| Setting                                     | Description                                                                                                                                                                                      |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Custom Variable Type**                    | Which type the step writes.                                                                                                                                                                      |
| **Custom Variable Identity**                | The matching rule: pick a **Custom Variable Field** and the trigger value it must equal (**Equals Value From**, e.g. `trigger.crm_id`). A match updates that record; no match creates a new one. |
| **Link custom variable to current contact** | Links the saved record to the contact the automation is processing, when one is available (for example, the contact from a Call Completed trigger).                                              |
| **Values To Save**                          | A field map from trigger/context values to the type's fields.                                                                                                                                    |

After the step runs, later steps in the same pipeline can read the saved values as `custom.<type name>.<field>` in their own field maps — see [Field Mapping & Context](/automations/field-mapping). A typical pattern: a webhook from your CRM fires an automation that upserts an Order record, links it to the contact, then creates a call — so the agent dials with `{{custom.order.due_date}}` (string, e.g. `2026-03-01`) already populated.

***

## Next Steps

* [Custom Variables Overview](/custom-variables/overview) — the workspace, record editing and the script-token copy button.
* [Custom Variable Types & Schema Validation](/custom-variables/types) — build type schemas and choose Off/Warn/Strict validation.
* [Importing Custom Variables from CSV](/custom-variables/csv-import) — typed CSV headers, templates and mapping rules.
* [Variables](/flows/variables) — the full variable reference, including the `custom` namespace and the `/` picker.
