# Data Mapping (/docs/connectors/data-mapping)



Provider payloads are all shaped differently — HubSpot's contact JSON looks
nothing like Salesforce's. **Data mappers** convert those provider-shaped payloads
into schema-versioned, normalized connector *models* before the data enters
workflow inputs, sync-record lookups, or routing strategy features. Downstream
steps then read a stable canonical result (`canonical.contact`) instead of
provider-specific JSON.

## What a mapper is [#what-a-mapper-is]

A mapper is a deterministic, registered transform from a provider payload to a
normalized model. The runtime executes **declarative, CEL, and Go-backed mappers
only** (`docs/connectors/data-mappers.md`). Imported template mappers that would
require TypeScript execution stay catalog-visible as **metadata-only** specs with
an explicit runtime reason — they are never silently skipped.

Inspect and exercise the catalog through the connector API:

* `ListConnectorModels` / `GetConnectorModel` — the normalized models a provider exposes.
* `ListConnectorMappers` / `GetConnectorMapper` — the registered mapper specs.
* `ValidateConnectorMapper` — check a mapper without running it.
* `PreviewConnectorMapper` — run a registered mapper against a sample payload; returns normalized bytes plus a metadata-only `mapper_execution` summary (mapper key/version, source kind/id, output model key/external id, status, bounded error code). It never carries raw provider input.

## Where mapping runs [#where-mapping-runs]

Mapping is bound at three points, all going through the same executor:

* **Action output.** An `ActionSpec` can declare `output_mapper_key` and
  `default_mapper_output_mode`; a caller can override per execution with
  `mapper_key` and `mapper_output_mode` (`raw` keeps provider bytes and skips the
  mapper, `normalized` returns mapper output, `both` returns provider bytes plus
  `normalized_output`). Responses also carry `normalized_model_key`,
  `normalized_external_id`, and `normalized_schema_hash`.
* **Sync records.** A sync execution strategy binds a `mapper_key` per model plan;
  each mapped record is stored as a `SyncRecord` with `model`, `external_id`,
  `content_hash`, and normalized `data`.
* **Webhook/lifecycle payloads.** A `TriggerSpec` can declare `payload_mapper_key`;
  ingress verifies and dedupes the delivery, then runs the mapper before starting
  the workflow, so the canonical event envelope arrives with `raw_payload` and
  `normalized_payload` side by side. See [Triggers & Polling](/docs/connectors/triggers-and-polling).

Whichever binding point invoked it, the output mode decides what the caller
actually receives — and `raw` is the one mode that skips the mapper entirely:

```mermaid
flowchart TD
  res[provider result] --> mode{output mode}
  mode -->|raw| out1([provider bytes, mapper skipped])
  mode -->|normalized| map1[run mapper] --> out2([normalized only])
  mode -->|both| map2[run mapper] --> out3([bytes + normalized_output])
```

<Callout type="info" title="Mapper bindings are a routing data contract">
  Mapper bindings are part of the function manifest hash. Changing an
  `output_mapper_key`, `payload_mapper_key`, or default mapper output mode changes
  the manifest/schema hash and surfaces in `DiffConnectorDeployment` as a
  function-level `schema_changed` / `manifest_changed` reason. Treat those diffs as
  data-contract changes, not cosmetic version bumps — see
  [Marketplace & Deployments](/docs/connectors/marketplace-and-deployments).
</Callout>

## Field catalogs and mapping profiles [#field-catalogs-and-mapping-profiles]

Where mappers *transform* payloads, **field catalogs** and **mapping profiles**
(`docs/connectors/field-catalogs-mapping-profiles.md`) make the tenant's
provider-schema choices durable and auditable instead of hiding them in generic
connection metadata.

* A **field catalog** is a scoped provider-schema snapshot for a tenant. Object
  catalogs track provider objects (contacts, tickets, boards, pipelines, custom
  objects); field catalogs track object fields, provider/normalized types,
  read/write mode, required/nullability flags, **PII class**, enum options,
  relationships, constraints, schema hashes, and **drift state**. Refresh through
  `RefreshConnectorFieldCatalog`; unsupported providers return an explicit
  `unsupported` / `metadata_only` status rather than pretending discovery is
  complete.
* **Dynamic property resolvers** answer the interactive question "what can a user
  pick *right now* for this form field?" They can feed discovery, but they are not
  the durable source of truth — the catalog is.
* A **mapping profile** is a versioned tenant field contract mapping an internal
  model field path to a provider object field path (read / write / bidirectional),
  with required/null policy, defaults, transform/mapper/dataflow refs, a source
  catalog hash, and a stable profile hash. Create a draft, validate it against the
  current catalog, preview with redacted samples, then promote to active.

<Callout type="warn" title="Promotion fails closed on breaking drift">
  Profile promotion is rejected when the source catalog is stale or breaking,
  required fields are unmapped, enum defaults are invalid, dependency refs are
  missing, policy/entitlement checks fail, or identity locks disagree. Breaking
  catalog drift blocks promotion outright; a stale non-breaking active profile can
  continue only when its governing policy allows it, and runtime paths never mutate
  a profile definition.
</Callout>

Sync runs, sync records, mapper executions, and provider-event envelopes all
record the profile id, version, and hash that shaped their payload — so replay and
promotion evidence can freeze the exact contract that produced a record.

## The connector function runtime [#the-connector-function-runtime]

A handful of imported template mappers, actions, syncs, and webhooks are authored
in TypeScript. Those run on the **connector function runtime**
(`docs/connectors/function-runtime.md`) — a governed TypeScript execution surface
that is **default-off** and **must** run through the exec-plane sandbox isolation
boundary. TypeScript function code is never executed in the Go process;
`in_process` is explicitly not an isolation boundary and startup fails closed if
the runtime is enabled without subprocess or remote executor isolation.

Its prototype runner (`tools/connector-function-runner`) reads an `ExecSpec` from
stdin and writes an `ExecResult` to stdout, and enforces hard invariants: no
raw secrets passed to the runner, no mutable source loaded from local checkouts at
execution, no network package installs, direct network `fetch` reported as
`direct_fetch_blocked`, and no raw provider payloads/PII in dry-run evidence.
There is intentionally no production-execute MCP tool yet — only status, describe,
and dry-run.

<Callout type="warn" title="Experimental">
  The connector function runtime is an experimental, default-off surface. Mappers
  should prefer the declarative, CEL, or Go path; a TypeScript mapper stays
  metadata-only until the runtime is deliberately enabled and wired.
</Callout>

## Where to go next [#where-to-go-next]

* [The Sync Engine](/docs/connectors/sync-engine) — the subsystem that stores mapper-derived normalized records.
* [Connector Strategy Contracts](/docs/strategies/contracts) — connector strategy fact profiles let strategies consume redacted, mapper-derived facts.
* [Marketplace & Deployments](/docs/connectors/marketplace-and-deployments) — how mapper bindings ride in an immutable deployment manifest.
