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

# Syncing Custom Objects from a Warehouse

> A guide for data administrators setting up a data warehouse sync into Conversion custom objects for the first time.

This guide is for data administrators who know their warehouse but are new to Conversion. It walks through planning and building a sync that loads warehouse tables into Conversion [custom objects](https://docs.conversion.ai/product-docs/records/custom-objects/overview.md). For column rules, schedules, and incremental syncing, see the [Data Warehouse Sync overview](https://docs.conversion.ai/product-docs/sync/data-warehouse/overview.md).

The short version: **recreate the tables you need as custom objects in Conversion, then write one `SELECT` per table that maps its columns onto those objects' fields.**

***

## How warehouse tables map to Conversion

Conversion stores records in objects that behave like tables. Contacts and companies are built in. Custom objects are tables you define. A sync reads rows from your warehouse and writes them as records of one object.

| In your warehouse | In Conversion |
| :- | :- |
| A table or view | A custom object, such as *Products* |
| A row | A record |
| The primary key | The record's object ID, selected as `object_id` |
| A column | A field, matched by its field key |
| A foreign key to another table | A relationship field on the object that holds the foreign key |
| A foreign key to your users table | A relationship field to Contacts, filled with the user's `userId` or email |

```mermaid theme={null}
flowchart LR
  subgraph WH["Your warehouse"]
    T1[("users")]
    T2[("dashboards")]
    T3[("products<br/>user_id, dashboard_id")]
  end
  subgraph CV["Conversion"]
    O1["Contacts"]
    O2["Dashboards<br/>custom object"]
    O3["Products<br/>custom object"]
  end
  T1 -->|"contacts sync"| O1
  T2 -->|"sync 1"| O2
  T3 -->|"sync 2"| O3
  O3 -.->|"contact relationship"| O1
  O3 -.->|"dashboard relationship"| O2
```

New to custom objects? Read [Understanding Custom Objects](https://docs.conversion.ai/product-docs/records/custom-objects/understanding-custom-objects.md) first. It explains records, object IDs, primary fields, and which side of a relationship holds the link.

***

## Set up a custom object warehouse sync

### Before you begin

* A [connected warehouse](https://docs.conversion.ai/product-docs/sync/data-warehouse/overview.md#1-connect-your-warehouse) with a read-only user that can see the tables you plan to sync
* Admin access in Conversion to create objects, fields, and syncs
* Custom objects enabled for your workspace. If you don't see the **Custom objects** section in **Settings → Objects**, contact Conversion.

### 1. Understand both data models and the goal

Before you write any SQL, get clear on two things: how data is modeled in Conversion, and what data you're bringing over.

* **Conversion's data model.** Read [Understanding Custom Objects](https://docs.conversion.ai/product-docs/records/custom-objects/understanding-custom-objects.md). Conversion centers on contacts: audiences, emails, and workflows all act on contacts. A custom object is useful in Conversion mainly when its records link, directly or through other objects, to contacts or companies.
* **Your data.** List the tables you're considering, their primary keys, and the foreign keys between them. Note which table holds your users and which column identifies them (a user ID, an email, or both).

Then talk with your MOps leader about what they want to do with the data. The goal decides which tables, columns, and relationships you need to bring over, so agree on the end structure before you build anything. Useful questions:

| Ask | Why it matters |
| :- | :- |
| Which audiences or segments do you want to build? | Every filter they need must exist as a field, and every object it crosses must be linked by a relationship. |
| Which changes should trigger workflows? | A workflow can watch a custom object field only if the record links to contacts. |
| What should emails show? | Each value you want to personalize with must be a field on a linked record. |
| Which records matter? All products, or only active ones? | Filter in your `WHERE` clause instead of syncing rows nobody uses. |
| How fresh does the data need to be? | Sets the [sync schedule](https://docs.conversion.ai/product-docs/sync/data-warehouse/overview.md#3-set-the-schedule). |

Sketch the result as a small diagram of objects and their relationships, like the one above. Bring over only what the goals need. You can add fields and objects later.

### 2. Create the custom objects

1. In **Settings → Objects**, [create one custom object](https://docs.conversion.ai/product-docs/workspace-settings/objects/overview.md#creating-a-custom-object) for each table you're syncing. Don't link it to a Salesforce object.
2. On each object's **Fields** tab, [create a field](https://docs.conversion.ai/product-docs/workspace-settings/objects/fields.md#creating-a-field) for each column you want to sync. Unlike Salesforce syncs, warehouse syncs don't create fields for you. Note each field's **Key**: your query must use it as the column name.
3. On each object's **Overview** tab, choose the **Primary field**, usually the column that holds the record's name.

You don't create a field for the primary key. Every custom object record already has an object ID, and your query fills it with the `object_id` column.

### 3. Create the relationship fields

For each foreign key you want to keep, [create a relationship field](https://docs.conversion.ai/product-docs/workspace-settings/objects/fields.md#relationship-fields) on the object whose table holds the foreign key (the child), pointing at the object the key refers to (the parent).

In the example above, `products` has `user_id` and `dashboard_id` columns, so *Products* gets two relationship fields:

| Relationship field on *Products* | Points at | Key | Value in the sync |
| :- | :- | :- | :- |
| **Contact** | Contacts | `contact` | The user's email or `userId` |
| **Dashboard** | *Dashboards* | `dashboard` | The dashboard's `object_id` |

Conversion adds a matching list field to each parent (a contact's **Products**, a dashboard's **Products**). You never sync into those. They fill in from the child's field.

> [!WARNING]
>
> Create the relationship field on the child, not the parent. If it ends up on the parent, your query has nowhere to put the foreign key, and the sync can't set the link.

### 4. Write one sync per object

[Create a sync](https://docs.conversion.ai/product-docs/sync/data-warehouse/overview.md#2-create-a-sync) for each custom object, with **Custom Objects** as the destination type and the object selected. Each query must:

* Select the table's primary key `AS object_id`. The sync editor flags a query without it (`id` also works).
* Name every other column with the Conversion field key, using `AS` when the names differ. Columns that match no field are flagged in the editor.
* Select each foreign key `AS` the key of its relationship field.

Sync parents before children. Start with the object that has no relationships to other custom objects:

```sql theme={null}
-- Sync 1: Dashboards
SELECT
  id AS object_id,
  name,
  created_at
FROM analytics.dashboards
WHERE updated_at > TO_TIMESTAMP({{last_sync_time}})
```

Then sync the child, linking each row to its parents:

```sql theme={null}
-- Sync 2: Products
SELECT
  p.id AS object_id,
  p.name,
  p.seats,
  p.renews_at,
  u.email AS contact,           -- relationship field to Contacts
  p.dashboard_id AS dashboard   -- relationship field to Dashboards: its object_id
FROM analytics.products p
JOIN analytics.users u ON p.user_id = u.id
WHERE p.updated_at > TO_TIMESTAMP({{last_sync_time}})
```

`p.dashboard_id` works as the link because it holds the same values that sync 1 selected as the dashboards' `object_id`. A link to a custom object must always carry the related record's `object_id`, never its name or another column.

The timestamp syntax differs by warehouse. See your warehouse's guide: [BigQuery](https://docs.conversion.ai/product-docs/sync/data-warehouse/big-query.md), [Snowflake](https://docs.conversion.ai/product-docs/sync/data-warehouse/snowflake.md), [ClickHouse](https://docs.conversion.ai/product-docs/sync/data-warehouse/clickhouse.md), or [Databricks](https://docs.conversion.ai/product-docs/sync/data-warehouse/databricks.md).

### 5. Check the results

1. Run each sync and open its history on the **Syncs** tab to check rows processed and any errors.
2. Open a few records in the custom object's records list and confirm their fields and relationship chips.
3. Open a linked contact and confirm the related records appear in the matching field on its **Details** tab.
4. Share the result with your MOps leader and confirm it supports the audiences, workflows, and emails you agreed on.

Onboarding with Conversion? Let your Deployment Strategist know when this is done and we'll check it on our end.

***

## How links resolve

```mermaid theme={null}
sequenceDiagram
  participant S1 as Sync 1 (Dashboards)
  participant S2 as Sync 2 (Products)
  participant C as Conversion
  S1->>C: dash_42 (object_id)
  S2->>C: prod_7, dashboard = dash_42, contact = ana@acme.com
  C->>C: finds dashboard dash_42 and links it
  C->>C: finds contact ana@acme.com and links it
  S2->>C: prod_8, dashboard = dash_99
  C->>C: dash_99 not found yet, link stays pending
  S1->>C: dash_99 (next run)
  C->>C: pending link completes
```

* **Order matters, but not strictly.** If the related record doesn't exist yet, the link stays pending and completes if that record arrives within a few days. Syncing parents first avoids the wait.
* **Syncs never create the related record.** A product linked to `ana@acme.com` doesn't create that contact. Sync your contacts too, or make sure they already exist in Conversion.
* **Contacts link by `userId` or email.** A relationship holds one record, so if several contacts share an email, the link reaches only one of them. Prefer `userId` when your contacts sync uses it.
* **Companies link by Salesforce Account ID.**
* **One value per row.** A relationship field holds one related record. If several rows share an `object_id`, they overwrite each other's values.

***

## Common mistakes

| Symptom | Likely cause | Fix |
| :- | :- | :- |
| The editor says the query must select a column aliased as `object_id` or `id` | The primary key isn't selected, or has another name | Add `your_key AS object_id` |
| A column is flagged as matching no field | The column name differs from the field key, or the field doesn't exist yet | Create the field, or alias the column to the field's **Key** |
| Records sync but have no links | The relationship field is missing from the query, or its value doesn't match any record | Select the foreign key `AS` the relationship field's key, with the value type from [How links resolve](#how-links-resolve) |
| There's no relationship field to alias the foreign key to | The relationship field was created on the parent | Delete it and create it on the child |
| Links to another custom object never complete | The value isn't that record's `object_id`, or its sync hasn't run | Link by the related table's primary key, and run the parent sync |
| Fewer records than rows | Several rows share an `object_id` | Make the query return one row per record |

***

## Related

* [Understanding Custom Objects](https://docs.conversion.ai/product-docs/records/custom-objects/understanding-custom-objects.md): the mental model
* [Custom Objects](https://docs.conversion.ai/product-docs/records/custom-objects/overview.md): full reference
* [Data Warehouse Sync overview](https://docs.conversion.ai/product-docs/sync/data-warehouse/overview.md): column rules, schedules, and incremental syncing


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.