---
name: unified-data-mapping
description: >-
  Map Unified.to objects (crm_contact, ats_candidate, hris_employee,
  accounting_invoice, …) to an app's own database tables or domain model, so
  the same code works across every integration: per-integration readable and
  writable fields, required fields on create, custom fields through `metadata`
  and the Metadata API, vendor-only fields through `raw`, record IDs and upsert
  keys. Use when a developer asks to "store CRM contacts in our database", "map
  Unified fields to our schema", "sync custom fields", or "get a field Unified
  doesn't have".
license: MIT
metadata:
  category: data-mapping
  api_base: https://api.unified.to
  docs: https://docs.unified.to/reference/fields
  docs_index: https://docs.unified.to/llms.txt
---

# Map Unified.to objects to your app's data model

Unified.to returns the same object shape for every integration in a category,
so an app maps each unified object to its own schema once. What differs per
integration is which fields are actually populated or writable, and where
custom and vendor-specific fields live. This skill produces a mapping that
holds up across the integrations the app offers.

Sources: https://docs.unified.to/reference/fields,
https://docs.unified.to/metadata/overview,
https://docs.unified.to/reference/slow-fields and the model page of each
object (e.g. https://docs.unified.to/crm/contact/model.md). If something here
disagrees with them, the docs win.

## Step 1: Gather the inputs

Ask in one batch:

1. **The app's schema:** the tables or types to fill, with their columns
   (paste the migration, ORM model or type definitions).
2. **Unified objects:** which `<category>_<object>` feeds each table.
3. **Direction:** read only, or also write back to the integration.
4. **Integrations** the app will offer, by integration type.
5. **Custom fields:** whether the app needs the end user's custom fields
   (e.g. a custom "Industry" field on a CRM contact).

## Step 2: Read the unified model and per-integration support

- **Model:** read the object's model page
  (`https://docs.unified.to/<category>/<object>/model.md`) for every field,
  its type and nested shapes, e.g. `emails` and `telephones` are arrays of
  objects, not strings. Dates are ISO 8601 strings in UTC.
- **Support per integration:** look up each integration (Core MCP
  `list_unified_integrations` with `type`, or
  `GET https://api.unified.to/unified/integration?type=<integration_type>`, or
  `https://docs.unified.to/integrations/<integration_type>.md`) and read
  `support.<object>.outbound_fields` (readable), `inbound_fields` (writable;
  `supported-required` must be sent on create) and `slow_fields`.

## Step 3: Write the mapping table

One row per app column (the values below are illustrative; fill them from Step 2):

| App column | Unified field | Transform | Read by | Writable by | Required on create |
| --- | --- | --- | --- | --- | --- |
| `contacts.full_name` | `name` | none | all | all | salesforce |
| `contacts.email` | `emails[0].email` | first email, lower-cased | all | all | none |
| `contacts.industry` | `metadata[slug=industry].value` | string | hubspot, salesforce | hubspot, salesforce | none |
| `contacts.external_id` | `id` | none | all | n/a | n/a |

- "Read by" and "Writable by" come from Step 2, so a column that some
  integrations can't fill is visible up front. Make those columns nullable,
  or hide the feature for those integrations; say which to the developer.
- Keep transforms out of the HTTP code; put them in one mapping function per
  object so every integration goes through the same code.
- Don't map fields the app doesn't use; request only the ones you map (Step 6).

## Step 4: IDs and upserts

- Store the unified record's `id` together with the connection ID; the pair
  is the upsert key. IDs come from the vendor, so treat them as unique only
  within one connection.
- If you need the vendor's original ID (for example to call its API through
  Passthrough), it is in `raw.__id` when Unified.to had to transform the ID.
- Use `created_at` / `updated_at` from the record, not your own clock, to
  decide whether an incoming record is newer than what you stored.
- Link records to your own user or account with the connection's
  `external_xref`, which also comes back on webhooks.

## Step 5: Custom fields (`metadata`)

Custom fields (the end user's own fields on a vendor object) are exposed on
many objects as a `metadata` array:

```json
{
  "id": "contact1",
  "name": "Jane Doe",
  "metadata": [{ "slug": "industry", "value": "Software", "format": "TEXT" }]
}
```

- **Discover definitions** with the Metadata API:
  `GET /metadata/{connection_id}/metadata?type=crm_contact` returns each
  custom field's `name`, `slug`, `format`, `options` and `is_required`. Let
  the end user pick which custom fields map to which app columns; slugs differ
  per connection (e.g. `Region__c` in Salesforce).
- **Read values** from `metadata[].value`, typed by `format`: DATE as ISO
  8601, NUMBER / PRICE / CURRENCY as numbers, BOOLEAN / YES_NO as booleans,
  MULTIPLE_SELECT as an array, otherwise a string.
- **`metadata` is often a slow field.** If the workspace skips slow fields by
  default, include `metadata` in `fields` to receive it.
- **Write values** by creating or updating the parent object with a
  `metadata` array whose entries have the definition's `id` and/or `slug`,
  plus `value`. Unknown slugs are rejected by the integration.
- Which objects support `metadata` depends on the integration; check
  `outbound_fields.metadata` / `inbound_fields.metadata` in Step 2.

## Step 6: Fields the unified model doesn't have (`raw`)

Use, in this order:

1. **The Metadata API** for the end user's custom fields (Step 5).
2. **`raw`** for the vendor's own fields:
   - Read: add `raw` to `fields`
     (`?fields=id,name,emails,raw`). Some vendors (e.g. HubSpot,
     Salesforce) only return a field if you name it, so add
     `raw.<field>` too: `?fields=id,name,raw,raw.leadFunnelStage`.
   - Write: include a `raw` object in the create or update body,
     e.g. `{ "name": "John Doe", "raw": { "custom_integration_field": "value" } }`.
   - `raw` is vendor-specific, so code that uses it only works for that
     vendor. Keep it in vendor-specific branches of the mapping.
3. **Passthrough** for vendor endpoints outside the unified API. Contact
   Unified.to before building on it; most gaps can be closed in the unified
   API instead.

Use `fields` to request only what you map, e.g.
`?fields=id,name,emails,updated_at`. It keeps responses small and skips slow
fields you don't need. The same `fields` option works on webhooks.

## Step 7: Loading data

- **Initial import:** page through the list endpoint with `limit` (max 100)
  and `offset`, or create a webhook with `include_all=true` to receive all
  existing records (see the `unified-webhooks` skill).
- **Changes:** webhooks, or polling with `updated_gte` where
  `list_updated_gte` is supported.
- Upsert every record by the key from Step 4. Duplicates and out-of-order
  deliveries happen.

## Step 8: Present the mapping and wait

Show the developer the mapping table, the columns some integrations can't
fill, the custom-field plan and any `raw` usage. Wait for approval before
writing migrations or code.

## Related skills

- Which vendors support the feature: https://docs.unified.to/skills/unified-integration-planner/SKILL.md
- Webhooks and backfill: https://docs.unified.to/skills/unified-webhooks/SKILL.md
- Object shapes per category, e.g. https://docs.unified.to/skills/unified-crm/SKILL.md
