Skip to main content

The reverse-ETL destination model

Reverse-ETL is one of the four export families — the one that continuously replicates your contacts entity, your CDP event stream, and your computed profile traits into a warehouse you own. The families page gives you the map; this page is the destination catalog itself: what the three capability verdicts mean on GET /api/v1/cdp/reverse-etl/profile-exports, how the push cadence interacts with trait recompute, and how per-destination run-status on GET /api/v1/cdp/reverse-etl/sync-runs surfaces failures instead of failing silent.

1. Where reverse-ETL sits in the export families map

Reverse-ETL is the scheduled, continuous export — the only family whose value is repetition rather than the artifact:
  • The ad-hoc CSV and the conversation vCon produce a one-time download; reverse-ETL produces a replicated table inside your warehouse that stays current on a cadence.
  • WORM archival proves records have not changed; reverse-ETL rewrites your downstream copy so it keeps matching the live one.
  • Pick reverse-ETL when your BI, ML, or finance tooling should query Orbit data where you already keep the rest of your analytics. Pick a different family when the question is one file, one thread, or one audit (the §1 table in the export families model is the decision).
The shipped catalog is six destinations: BigQuery, Snowflake, Redshift, Postgres, Databricks, and ClickHouse. You configure and enable each under your CDP destinations panel, and per-destination secrets (service-account JSON, connection credentials) are stored encrypted and never returned by any read surface — read surfaces expose only non-secret config state and run-status fields.

2. The destination catalog: three capability verdicts

Not every destination treats your computed profile traits the same way downstream. The profile-exports read model classifies each configured destination into exactly one of three verdicts, and reports it as profile_export_capability on GET /api/v1/cdp/reverse-etl/profile-exports: Each destination row on the response joins that verdict against your computed-trait catalog, so per trait you get a propagation decision, not just a tier:
  • syncable: true requires a native verdict on a configured and enabled destination, and a trait in active status.
  • Otherwise syncable: false with an explicit block_reason: destination_not_configured, destination_disabled, traits_via_exported_data (the entire organic tier), or trait_not_active (paused traits keep their last materialised value; archived is terminal).
Two consequences worth planning against:
  • This is a capability model, not a second pipeline. The verdict is derived from destination config plus the trait catalog you already have — it cannot drift from what the exporters actually write. Query it once per destination instead of inferring per-destination behavior from a spreadsheet.
  • A trait that never propagates is surfaced with its reason, not absent. Paused and archived traits still appear in the per-destination rows with block_reason: "trait_not_active", so a trait you paused stops moving downstream visibly rather than vanishing from the panel.

3. Schedule semantics: a cadence, not a request

Reverse-ETL pushes run on a schedule per destination, not on demand — the schedule is the whole point of the family:
  • Default cadence is ~nightly. Unconfigured intervals inherit the pre-feature gap of 23 hours, with scheduled runs firing only after 02:00 UTC so the previous day’s upstream writes have settled.
  • The interval is yours to trade off. Set a per-destination sync interval from 1 hour up to 30 days. Destinations at the daily-or-slower end keep the 02:00-UTC settle window; hourly-or-faster destinations run around the clock.
  • “Sync now” exists but does not change the model. A sync-now request makes the destination due at the next scheduler tick — it brings the normal forward delta early rather than changing what gets exported, and a failed run keeps the request set so it retries.
How the cadence interacts with computed-trait recompute: computed traits re-derive on their own pipeline (per the segment recompute lifecycle — the daily enrichment sweep is the slowest ingredient). A scheduled export carries the materialised trait value at run time. Two operators’ rules follow:
  1. Warehouse freshness is bounded by the export cadence, not the trait pipeline. Setting a 1-hour export interval does not make daily-enriched traits hourly; it ships the current materialised value more often.
  2. Treat the warehouse copy as eventually consistent. When freshness matters for a downstream model, read the destination’s last_run_at on the sync-runs surface rather than assuming the hourly interval held.

4. Failure bookkeeping: per-destination run-status

Each successful or failed run writes its own per-destination run-status back onto that destination’s config state, and the unified read surface at GET /api/v1/cdp/reverse-etl/sync-runs projects the latest snapshot for all six destinations in one response. Per destination row:
  • last_run_at — when the most recent run ran.
  • last_run_statusok or failed.
  • rows_contacts / rows_events (with rows_total as a convenience sum) — exported row counts per table.
  • last_error + last_error_at, consecutive_failures — the failure detail.
  • health — a derived single-glance verdict, resolved most-actionable-first: disablednever_runfailingstaleok.
The two loud verdicts exist so a broken export is never silent:
  • failing — the most recent run returned failed; the row carries the last error message and even a nightly scheduler hiccup is visible in one query instead of buried in per-destination config.
  • stale — the last run succeeded but is older than 48 hours. The 48h window leaves one full missed-night of slack against the default 23h cadence, so the flag means “the scheduler genuinely stopped firing,” not “last night’s run is late.”
The response’s summary block rolls this up (enabled, failing, stale, needs_attention) so a dashboard or alert can key on one boolean. A destination you paused on purpose reports health: "disabled"; a brand-new one reports never_run until its first run lands.

5. Guarantees (inherited from the families model)

Reverse-ETL inherits the safety model every export family shares — three guarantees, stated once on the families page and linked here rather than restated:
  • Tenant-scoped. Every push exports documents from your own tenant schema; a cross-tenant export is not something the API can do.
  • Per-record ownership checks. A record outside your workspace never enters the export.
  • Time-bounded, signed URLs where applicable. Any generated artifact URL is time-bounded — mirror the data into your own store rather than relying on the link staying live.
Both read surfaces above are read-only and return no secret material — the verdict and run-status are projections over your destination config, never the credentials themselves.

6. When reverse-ETL is the wrong tool

  • A one-off audience pull — pick the ad-hoc CSV family; standing up a destination for a single CSV wastes the destination’s schedule.
  • Retention evidence / audit — pick the WORM archival family; reverse-ETL keeps your downstream copy current, it does not prove a record has not changed.
  • Sub-minute freshness — pick a streaming event sink (the CDP event stream’s always-on delivery) rather than the warehouse cadence, which deliberately tolerates single-night staleness before flagging stale.

Cross-references