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

# Unification

> How Cargo merges records from multiple integrations into unified account, contact, and event models

Unification is the process of merging records from your connected integrations and [object models](/models/object-models) into a single, deduplicated view. When you connect multiple data sources (CRMs, enrichment tools, analytics platforms) or write into native `Account` / `Contact` object models, Cargo automatically resolves and links records that represent the same real-world entity using shared identifiers called **references**.

The result is four native data models that serve as the canonical source of truth for your workspace: **Account**, **Contact**, **Account Event**, and **Contact Event**.

Unification runs automatically — there's nothing to deploy. You consume the unified models like any other, from the CLI or SQL:

```bash theme={null}
cargo-ai storage model list                    # → the unified Account / Contact models
cargo-ai storage model get-ddl <model-uuid>    # exact column names + types
cargo-ai storage query execute \
  "SELECT id, domain FROM object.accounts LIMIT 10"
```

***

## Reference a unified model in code

The four unified models are generated by Cargo's unification — they can't be created. With the [CDK](/get-started/project-layout), declaring one with its unify extractor **adopts** the live model instead of creating it: deploy binds the handle and reconciles its updatable surface (name, description, additional columns, sync schedule, and — on the entity models — the unification settings `config`), and destroy releases it. The handle then works anywhere a model is referenced:

```ts models/contacts.ts theme={null}
import { defineModel, defineSegment } from "@cargo-ai/cdk";

export const contacts = defineModel("contacts", {
  kind: "native",
  extractSlug: "unifyContacts",
});

export const reachable = defineSegment("reachable_contacts", {
  model: contacts,
  filter: {
    conjonction: "and",
    groups: [
      {
        conjonction: "and",
        conditions: [
          {
            kind: "string",
            columnSlug: contacts.columns.work_email,
            operator: "isNotEmpty",
          },
        ],
      },
    ],
  },
});
```

The unify extractor slugs are `unifyAccounts`, `unifyContacts`, `unifyAccountEvents`, and `unifyContactEvents` — one per unified model.

***

## How unification works

<Steps>
  <Step title="Connect data sources">
    Connect integrations (HubSpot, Salesforce, LinkedIn, etc.) that sync data models into your warehouse. Each integration model declares which **unification type** it participates in — `account`, `contact`, `accountEvent`, or `contactEvent`.
  </Step>

  <Step title="Map references">
    Each source model maps its columns to **unification references** — shared identifiers like `domain`, `email`, or `linkedinId`. Cargo uses these references to determine which records across different sources represent the same entity.
  </Step>

  <Step title="Resolve canonical IDs">
    Cargo builds a mapping table that groups records sharing the same reference values under a single **canonical ID**. If a HubSpot company and a Salesforce account share the same domain, they receive the same canonical ID.
  </Step>

  <Step title="Materialize unified models">
    The native unified models are materialized as tables in your warehouse, refreshed every 12 hours by default. The sync schedule is editable per model, down to a minimum interval of 5 minutes.
  </Step>
</Steps>

***

## Unified models

### Account

The Account model merges company records from all connected integrations into a single, deduplicated table. Each row represents a unique company, identified by a canonical ID computed from shared references.

| Column                 | Type     | Description                                                         |
| :--------------------- | :------- | :------------------------------------------------------------------ |
| `id`                   | `string` | Canonical account ID — unique identifier for this unified company   |
| `domain`               | `string` | Company website domain                                              |
| `slug`                 | `string` | Company name slugified (lowercased, suffixes stripped, dash-joined) |
| `linkedin_id`          | `string` | LinkedIn company numeric ID                                         |
| `linkedin_handle`      | `string` | LinkedIn company vanity URL slug                                    |
| `crunchbase_uuid`      | `string` | Crunchbase organization UUID                                        |
| `crunchbase_permalink` | `string` | Crunchbase organization permalink                                   |
| `twitter_handle`       | `string` | X / Twitter company handle                                          |
| `sales_navigator_id`   | `string` | LinkedIn Sales Navigator account ID                                 |
| `ids`                  | `object` | Map of source connector models to their original record IDs         |

<Note>
  Reference columns (`domain`, `linkedin_id`, etc.) are resolved by taking the highest-priority non-null value across all source records that share the same canonical ID.
</Note>

### Contact

The Contact model merges people records from all connected integrations into a single, deduplicated table. Each row represents a unique person, linked to their parent Account via `account_id`.

| Column                 | Type     | Description                                                      |
| :--------------------- | :------- | :--------------------------------------------------------------- |
| `id`                   | `string` | Canonical contact ID — unique identifier for this unified person |
| `email`                | `string` | Generic email address                                            |
| `personal_email`       | `string` | Personal email address                                           |
| `work_email`           | `string` | Work / professional email address                                |
| `linkedin_id`          | `string` | LinkedIn member numeric ID                                       |
| `linkedin_handle`      | `string` | LinkedIn member vanity URL slug                                  |
| `crunchbase_uuid`      | `string` | Crunchbase person UUID                                           |
| `crunchbase_permalink` | `string` | Crunchbase person permalink                                      |
| `twitter_handle`       | `string` | X / Twitter personal handle                                      |
| `sales_navigator_id`   | `string` | LinkedIn Sales Navigator lead ID                                 |
| `account_id`           | `string` | Foreign key to the unified Account `id`                          |
| `ids`                  | `object` | Map of source connector models to their original record IDs      |

<Note>
  The `account_id` is resolved through the parent unification mapping — if a source contact model declares a parent relationship to an account model, Cargo automatically links the unified contact to the correct unified account.
</Note>

### Account Event

The Account Event model aggregates event data from all connected integrations into a single timeline, where each event is linked to its parent Account.

| Column         | Type     | Description                                                      |
| :------------- | :------- | :--------------------------------------------------------------- |
| `id`           | `string` | Composite event ID (`<dataset_slug>__<model_slug>__<record_id>`) |
| `account_id`   | `string` | Foreign key to the unified Account `id`                          |
| `occurred_at`  | `date`   | Timestamp when the event occurred (from the source time column)  |
| `event_name`   | `string` | Name of the event, derived from the source model name            |
| `properties`   | `object` | Original column values from the source record, keyed by slug     |
| `dataset_slug` | `string` | Slug of the source database / dataset                            |
| `model_slug`   | `string` | Slug of the source model within the dataset                      |

### Contact Event

The Contact Event model aggregates event data from all connected integrations into a single timeline, where each event is linked to its parent Contact.

| Column         | Type     | Description                                                      |
| :------------- | :------- | :--------------------------------------------------------------- |
| `id`           | `string` | Composite event ID (`<dataset_slug>__<model_slug>__<record_id>`) |
| `contact_id`   | `string` | Foreign key to the unified Contact `id`                          |
| `occurred_at`  | `date`   | Timestamp when the event occurred (from the source time column)  |
| `event_name`   | `string` | Name of the event, derived from the source model name            |
| `properties`   | `object` | Original column values from the source record, keyed by slug     |
| `dataset_slug` | `string` | Slug of the source database / dataset                            |
| `model_slug`   | `string` | Slug of the source model within the dataset                      |

***

## References

References are the shared identifiers Cargo uses to match records across integrations. Each unification type supports a specific set of references. Event models inherit the references of their parent type.

### Account references

Used by the **Account** and **Account Event** models.

| Reference                | Description                                                    |
| :----------------------- | :------------------------------------------------------------- |
| **Domain**               | Company website domain (e.g. `acme.com`)                       |
| **Slug**                 | Company or billing-entity name, slugified — matching is opt-in |
| **LinkedIn ID**          | LinkedIn company numeric ID                                    |
| **LinkedIn Handle**      | LinkedIn company vanity URL slug                               |
| **Crunchbase UUID**      | Crunchbase organization UUID                                   |
| **Crunchbase Permalink** | Crunchbase organization permalink                              |
| **Twitter Handle**       | X / Twitter company handle                                     |
| **Sales Navigator ID**   | LinkedIn Sales Navigator account ID                            |

### Contact references

Used by the **Contact** and **Contact Event** models.

| Reference                | Description                       |
| :----------------------- | :-------------------------------- |
| **Email**                | Generic email address             |
| **Personal Email**       | Personal email address            |
| **Work Email**           | Work / professional email address |
| **LinkedIn ID**          | LinkedIn member numeric ID        |
| **LinkedIn Handle**      | LinkedIn member vanity URL slug   |
| **Crunchbase UUID**      | Crunchbase person UUID            |
| **Crunchbase Permalink** | Crunchbase person permalink       |
| **Twitter Handle**       | X / Twitter personal handle       |
| **Sales Navigator ID**   | LinkedIn Sales Navigator lead ID  |

***

## Slug matching

The **Slug** reference is the account's name, normalized. **Slug matching is opt-in** — it is `"none"` by default and does nothing until you enable it in the unification settings below. When enabled, the slug participates like any other weak identifier: identical normalized names corroborate a merge, but never cause one on their own. The slug reference exists on accounts only: person names need different matching semantics (a first+last+domain composite), so contacts carry no slug reference yet.

**Slugification.** Company names are lowercased, punctuation is stripped, a leading "the" is dropped, trailing legal-entity tokens (`Inc`, `LLC`, `GmbH`, `OpCo`, …) are removed, and the remaining tokens are dash-joined — so `OpenAI, Inc.` and `OpenAI OpCo LLC` both slugify to `openai`, and `The Coca-Cola Company` to `coca-cola`. Brand words like "Labs" are kept (Bell Labs is not Bell), so name variants stay distinct.

**Turning it on.** Set the slug to `"weak"` in the unification settings below. It then joins the identifier graph at the same trust level as a Twitter handle: weak identifiers never merge records on their own and can never bridge two strong components, so two accounts with different domains are never merged just because their names look alike. Placeholder slugs (`test`, `unknown`, `n-a`, …) and slugs shared by too many records are ignored entirely.

### Unification settings

How references match is configurable per unified model, through the model's extractor config — via the API (`PUT /v1/storage/models/:uuid` with `config`) or in code:

```ts models/accounts.ts theme={null}
import { defineModel } from "@cargo-ai/cdk";

export const accounts = defineModel("accounts", {
  kind: "native",
  extractSlug: "unifyAccounts",
  config: {
    domain: "strong",
    // Opt in to slug matching (identical normalized slugs only).
    slug: "weak",
    linkedinId: "strong",
    linkedinHandle: "weak",
    crunchbaseUuid: "strong",
    // Override a reference's merge trust.
    crunchbasePermalink: "strong",
    // Keep the golden column, but never merge on it.
    twitterHandle: "none",
    salesNavigatorId: "strong",
  },
});
```

The config is the complete map of reference → how it merges. There are three settings:

| Setting    | Meaning                                                                                      |
| :--------- | :------------------------------------------------------------------------------------------- |
| `"strong"` | Merges records on its own — forms the backbone of a unified entity.                          |
| `"weak"`   | Only corroborates a merge; can never bridge two records that disagree on a strong reference. |
| `"none"`   | Keeps the golden column populated, but never merges on it.                                   |

Every reference must be present. New models are seeded with the curated defaults — `domain`, `email`, and the ID-style references are `strong`, handles and permalinks are `weak`, and the `slug` is `none` (matching is opt-in). Updates must send the complete map back — read the model's current `config`, adjust the entries you want, and PUT the whole object back.

<Note>
  The highest-precision way to resolve a name-only record remains enrichment:
  resolve the name to a domain with an enrichment play, and the deterministic
  domain match takes over from there.
</Note>

***

## Reference priority

When multiple references match, Cargo uses an **ordered reference list** to determine which reference takes priority for deduplication. You can configure this order per unified model in the workspace settings.

For example, with the default account reference order `[domain, linkedinId, linkedinHandle, ...]`, two records sharing the same `domain` will be merged even if their `linkedinId` values differ.

***

## Relationships

The four unified models form a connected graph:

| Relationship            | Column       | Type        | Description                                         |
| :---------------------- | :----------- | :---------- | :-------------------------------------------------- |
| Contact → Account       | `account_id` | Many-to-one | Each contact is linked to one unified account       |
| Account Event → Account | `account_id` | Many-to-one | Each account event is linked to one unified account |
| Contact Event → Contact | `contact_id` | Many-to-one | Each contact event is linked to one unified contact |

***

## Source tracking

Every unified record includes an `ids` column — a JSON object that maps back to the original records from each contributing integration:

```json theme={null}
{
  "<dataset_slug>__<model_slug>": "<original_record_id>",
  "hubspot__companies": "12345",
  "salesforce__accounts": "001Ab00000XyZ"
}
```

For event models, the `id` itself is a composite key that encodes the source: `<dataset_slug>__<model_slug>__<original_record_id>`.

The `properties` column on event models preserves all original columns from the source record as a JSON object, so no data is lost during unification.
